Files
nebula/views/dashboard.templ

743 lines
31 KiB
Plaintext
Raw Permalink Normal View History

package views
import (
"nebula/components"
"nebula/layouts"
"nebula/models"
)
templ DashboardPage(data models.DashboardData, activeTab string) {
@layouts.DashboardLayout("Dashboard - Sonr", layouts.WalletUser{Name: "Sonr Wallet", Address: "sonr1x9f...7k2m"}, activeTab) {
@dashboardStyles()
@DashboardContent(data, activeTab)
<!-- Drawers -->
@components.AllDrawers(components.DefaultTokenOptions(), "sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m")
@components.DrawerTriggers()
}
}
templ DashboardContent(data models.DashboardData, activeTab string) {
<div id="dashboard-content">
if activeTab == "" || activeTab == "overview" {
@OverviewPanel(data)
} else if activeTab == "transactions" {
@TransactionsPanel(data.Transactions)
} else if activeTab == "tokens" {
@TokensPanel(data.Tokens)
} else if activeTab == "nfts" {
@NFTsPanel(data.NFTs)
} else if activeTab == "activity" {
@ActivityPanel()
}
</div>
}
templ OverviewPanel(data models.DashboardData) {
<header style="margin-bottom: var(--wa-space-l);">
<div class="wa-flank">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-l">Overview</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">All assets across all networks</span>
</div>
<div class="quick-actions">
<wa-button variant="neutral" appearance="outlined" size="small" data-drawer-open="receive">
<wa-icon slot="start" name="arrow-down"></wa-icon>
Receive
</wa-button>
<wa-button variant="neutral" appearance="outlined" size="small" data-drawer-open="send">
<wa-icon slot="start" name="arrow-up"></wa-icon>
Send
</wa-button>
<wa-button variant="brand" size="small" data-drawer-open="swap">
<wa-icon slot="start" name="arrow-right-arrow-left"></wa-icon>
Swap
</wa-button>
</div>
</div>
</header>
<div class="wa-grid stats-grid">
<wa-card class="stat-card">
<div class="wa-flank">
<wa-avatar shape="rounded" style="background: var(--wa-color-primary-subtle);">
<wa-icon slot="icon" name="wallet" style="color: var(--wa-color-primary);"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-3xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Balance</span>
<span class="wa-cluster wa-gap-xs">
<span class="wa-heading-2xl">
<wa-format-number type="currency" currency="USD" value={ data.TotalBalance } lang="en-US"></wa-format-number>
</span>
<wa-badge variant="success" pill>+2.4%<wa-icon name="arrow-trend-up"></wa-icon></wa-badge>
</span>
</div>
</div>
</wa-card>
<wa-card class="stat-card">
<div class="wa-flank">
<wa-avatar shape="rounded" style="background: var(--wa-color-success-subtle);">
<wa-icon slot="icon" name="chart-line" style="color: var(--wa-color-success);"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-3xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">24h Change</span>
<span class="wa-heading-2xl" style="color: var(--wa-color-success);">{ data.Change24h }</span>
</div>
</div>
</wa-card>
</div>
<!-- Portfolio Performance Chart -->
<wa-card style="margin-bottom: var(--wa-space-l);">
<div slot="header" class="wa-flank">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-m">Portfolio Performance</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Asset allocation over time</span>
</div>
<wa-radio-group value="1w" orientation="horizontal">
<wa-radio appearance="button" value="1d">1D</wa-radio>
<wa-radio appearance="button" value="1w">1W</wa-radio>
<wa-radio appearance="button" value="1m">1M</wa-radio>
<wa-radio appearance="button" value="1y">1Y</wa-radio>
</wa-radio-group>
</div>
@components.StackedAreaChart("portfolio-chart", data.PortfolioChart)
</wa-card>
<div class="wa-grid" style="--min-column-size: 360px; gap: var(--wa-space-l);">
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Token Balances</span>
<wa-button appearance="plain" size="small">
<wa-icon name="plus"></wa-icon>
Add Token
</wa-button>
</div>
for _, token := range data.Tokens {
@TokenRow(token)
}
</wa-card>
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Recent Transactions</span>
<wa-button appearance="plain" size="small">View All</wa-button>
</div>
<table class="tx-table">
<thead>
<tr>
<th>Type</th>
<th>Asset</th>
<th>Amount</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
for _, tx := range data.Transactions {
@TransactionTableRow(tx)
}
</tbody>
</table>
</wa-card>
</div>
@ConnectedAccountsCard()
}
templ TokenRow(token models.Token) {
<div class="token-row">
<div class="token-info">
<wa-avatar initials={ token.Initials } style={ "--size: 40px; background: " + token.Color + ";" }></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ token.Name }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ token.Symbol }</span>
</div>
</div>
<div class="token-balance">
<div class="wa-stack wa-gap-0" style="align-items: flex-end;">
<span class="wa-heading-s">{ token.Balance }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ token.Value }</span>
</div>
</div>
<div class="token-actions">
<wa-dropdown>
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Actions"></wa-icon-button>
<wa-dropdown-item><wa-icon slot="icon" name="arrow-up"></wa-icon>Send</wa-dropdown-item>
<wa-dropdown-item><wa-icon slot="icon" name="arrow-down"></wa-icon>Receive</wa-dropdown-item>
<wa-dropdown-item><wa-icon slot="icon" name="arrow-right-arrow-left"></wa-icon>Swap</wa-dropdown-item>
</wa-dropdown>
</div>
</div>
}
templ TransactionTableRow(tx models.Transaction) {
<tr>
<td>
<span class={ "tx-type", tx.Type }>
if tx.Type == "receive" {
<wa-icon name="arrow-down"></wa-icon>
Receive
} else if tx.Type == "send" {
<wa-icon name="arrow-up"></wa-icon>
Send
} else {
<wa-icon name="arrow-right-arrow-left"></wa-icon>
Swap
}
</span>
</td>
<td>{ tx.Asset }</td>
<td>
<div class="wa-stack wa-gap-0">
<span style={ txAmountStyle(tx.Positive) }>{ tx.Amount }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ tx.USD }</span>
</div>
</td>
<td>
<div class="wa-stack wa-gap-0">
<span>{ tx.Date }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ tx.Time }</span>
</div>
</td>
<td>
<wa-copy-button value={ tx.Hash } copy-label="Copy hash"></wa-copy-button>
</td>
</tr>
}
func txAmountStyle(positive bool) string {
if positive {
return "color: var(--wa-color-success);"
}
return "color: var(--wa-color-danger);"
}
templ ConnectedAccountsCard() {
<wa-card style="margin-top: var(--wa-space-xl);">
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Connected Accounts</span>
<wa-button appearance="plain" size="small">
<wa-icon slot="start" name="plus"></wa-icon>
Create
</wa-button>
</div>
<div class="wa-grid" style="--min-column-size: 280px;">
@AccountCard("M", "Main Wallet", "sonr1x9f...7k2m", "var(--wa-color-primary)", true)
@AccountCard("T", "Trading", "sonr1k4m...9p3q", "var(--wa-color-neutral-400)", false)
@AccountCard("S", "Savings", "sonr1r7t...2w8x", "var(--wa-color-neutral-400)", false)
</div>
</wa-card>
}
templ AccountCard(initials string, name string, address string, color string, active bool) {
<div class="wa-flank" style="padding: var(--wa-space-s); background: var(--wa-color-surface-alt); border-radius: var(--wa-radius-m);">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials={ initials } style={ "--size: 40px; background: " + color + ";" }></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ name }</span>
<span class="address-mono">{ address }</span>
</div>
</div>
if active {
<wa-badge variant="success" pill>Active</wa-badge>
} else {
<wa-icon-button name="ellipsis-vertical" label="Options"></wa-icon-button>
}
</div>
}
templ TransactionsPanel(transactions []models.Transaction) {
@components.TxStyles()
<header style="margin-bottom: var(--wa-space-l);">
<div class="wa-flank">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-l">Transactions</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Activity history across all connected accounts</span>
</div>
<wa-button variant="neutral" appearance="outlined" size="small">
<wa-icon slot="start" name="arrow-down-to-line"></wa-icon>
Export
</wa-button>
</div>
</header>
@components.TxStatsGrid(components.DefaultTxStats())
<wa-card>
@components.TxFilterBar(components.DefaultTxFilter())
for _, group := range components.DefaultTxGroups() {
@components.TxDateGroupComponent(group)
}
@components.TxPagination(1, 82, 10)
</wa-card>
}
templ StatCard(label string, value string, subtitle string, variant string) {
<wa-card class="stat-card">
<div class="wa-stack wa-gap-3xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ label }</span>
<span class={ "wa-heading-l", templ.KV("change-positive", variant == "success"), templ.KV("change-negative", variant == "danger") }>{ value }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ subtitle }</span>
</div>
</wa-card>
}
templ TokensPanel(tokens []models.Token) {
<header style="margin-bottom: var(--wa-space-l);">
<div class="wa-flank">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-l">Tokens</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">All crypto assets across Sonr networks</span>
</div>
<div class="wa-cluster wa-gap-s">
<wa-button variant="neutral" appearance="outlined" size="small">
<wa-icon slot="start" name="plus"></wa-icon>
Import Token
</wa-button>
<wa-button variant="brand" size="small">
<wa-icon slot="start" name="arrow-right-arrow-left"></wa-icon>
Swap
</wa-button>
</div>
</div>
</header>
<div class="wa-grid stats-grid">
<wa-card class="stat-card">
<div class="wa-stack wa-gap-xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Portfolio Value</span>
<span class="wa-heading-2xl">
<wa-format-number type="currency" currency="USD" value="12847.32" lang="en-US"></wa-format-number>
</span>
<span class="wa-caption-s change-positive">
<wa-icon name="arrow-trend-up" style="font-size: 12px;"></wa-icon>
+$302.18 (2.4%) today
</span>
</div>
</wa-card>
<wa-card class="stat-card">
<div class="wa-stack wa-gap-xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Assets</span>
<span class="wa-heading-2xl">7</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">across 3 networks</span>
</div>
</wa-card>
</div>
<wa-card>
<div class="filter-bar">
<wa-input placeholder="Search tokens..." size="small" style="width: 200px;">
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
</wa-input>
</div>
<table class="tokens-table">
<thead>
<tr>
<th class="sortable">Asset <wa-icon name="sort" style="font-size: 10px; margin-left: 4px;"></wa-icon></th>
<th>Network</th>
<th class="sortable">24h Change <wa-icon name="sort" style="font-size: 10px; margin-left: 4px;"></wa-icon></th>
<th class="sortable">Holdings <wa-icon name="sort" style="font-size: 10px; margin-left: 4px;"></wa-icon></th>
<th class="sortable">Value <wa-icon name="sort" style="font-size: 10px; margin-left: 4px;"></wa-icon></th>
<th></th>
</tr>
</thead>
<tbody>
for _, token := range tokens {
@TokenTableRow(token)
}
</tbody>
</table>
</wa-card>
}
templ TokenTableRow(token models.Token) {
<tr>
<td>
<div class="token-cell">
<wa-avatar initials={ token.Initials } style={ "--size: 36px; background: " + token.Color + ";" }></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ token.Name }</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ token.Symbol }</span>
</div>
</div>
</td>
<td>
<span class="network-badge">
<wa-icon name="circle" variant="solid" style="font-size: 6px;"></wa-icon>
{ token.Network }
</span>
</td>
<td>
<span class={ templ.KV("change-positive", token.Positive), templ.KV("change-negative", !token.Positive) }>
if token.Positive {
<wa-icon name="arrow-up" style="font-size: 10px;"></wa-icon>
} else {
<wa-icon name="arrow-down" style="font-size: 10px;"></wa-icon>
}
{ token.Change }
</span>
</td>
<td>{ token.Balance } { token.Symbol }</td>
<td>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ token.Value }</span>
</div>
</td>
<td>
<div class="table-actions">
<wa-tooltip content="Send"><wa-icon-button name="arrow-up" label="Send"></wa-icon-button></wa-tooltip>
<wa-tooltip content="Receive"><wa-icon-button name="arrow-down" label="Receive"></wa-icon-button></wa-tooltip>
<wa-dropdown>
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="More"></wa-icon-button>
<wa-dropdown-item><wa-icon slot="icon" name="arrow-right-arrow-left"></wa-icon>Swap</wa-dropdown-item>
</wa-dropdown>
</div>
</td>
</tr>
}
templ NFTsPanel(nfts []models.NFT) {
<header style="margin-bottom: var(--wa-space-l);">
<div class="wa-flank">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-l">NFTs</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Your digital collectibles portfolio</span>
</div>
<div class="wa-cluster wa-gap-s">
<wa-button variant="neutral" appearance="outlined" size="small">
<wa-icon slot="start" name="eye-slash"></wa-icon>
Hidden (2)
</wa-button>
<wa-button variant="brand" size="small">
<wa-icon slot="start" name="plus"></wa-icon>
Import NFT
</wa-button>
</div>
</div>
</header>
<div class="wa-grid stats-grid">
@StatCard("Total NFTs", "12", "across 5 collections", "neutral")
@StatCard("Estimated Value", "$8,420.50", "+12.4% this week", "success")
@StatCard("Floor Value", "4.2 ETH", "$9,851.40", "neutral")
</div>
<wa-card>
<div class="filter-bar">
<wa-select placeholder="All Collections" size="small" style="min-width: 160px;" clearable>
<wa-option value="bayc">Bored Ape Yacht Club</wa-option>
<wa-option value="sonr">Sonr Genesis</wa-option>
</wa-select>
<wa-select placeholder="Sort by" value="recent" size="small" style="min-width: 140px;">
<wa-option value="recent">Recently Added</wa-option>
<wa-option value="value-high">Value: High to Low</wa-option>
<wa-option value="value-low">Value: Low to High</wa-option>
</wa-select>
</div>
<div class="nft-grid">
for _, nft := range nfts {
@NFTCard(nft)
}
</div>
</wa-card>
}
templ NFTCard(nft models.NFT) {
<div class="nft-card">
<div class="nft-image">
<img src={ nft.Image } alt={ nft.Name }/>
if nft.Badge != "" {
<div class="nft-badge">
<wa-badge variant={ nftBadgeVariant(nft.Badge) } pill>{ nft.Badge }</wa-badge>
</div>
}
<div class="nft-actions">
<wa-tooltip content="Send">
<wa-icon-button name="paper-plane" label="Send"></wa-icon-button>
</wa-tooltip>
<wa-tooltip content="Hide">
<wa-icon-button name="eye-slash" label="Hide"></wa-icon-button>
</wa-tooltip>
</div>
</div>
<div class="nft-info">
<div class="nft-collection">
if nft.Verified {
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
}
<span class="nft-collection-name">{ nft.Collection }</span>
</div>
<div class="nft-name">{ nft.Name }</div>
<div class="nft-price-row">
<span class="nft-floor">Floor: { nft.Floor }</span>
<span class="nft-value">{ nft.Value }</span>
</div>
</div>
</div>
}
func nftBadgeVariant(badge string) string {
if badge == "Listed" {
return "success"
}
if badge == "Rare" {
return "brand"
}
return "neutral"
}
templ ActivityPanel() {
@ActivityPanelWithData(models.DefaultMarketCapBubbleData())
}
templ ActivityPanelWithData(marketCapData []models.BubbleData) {
<header style="margin-bottom: var(--wa-space-l);">
<div class="wa-flank">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-l">Activity</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Sessions, apps, and recent actions</span>
</div>
<wa-button variant="neutral" appearance="outlined" size="small">
<wa-icon slot="start" name="rotate"></wa-icon>
Refresh
</wa-button>
</div>
</header>
<div class="wa-grid stats-grid">
@ActivityStatCard("desktop", "Active Sessions", "3", "var(--wa-color-primary)")
@ActivityStatCard("plug", "Connected Apps", "5", "var(--wa-color-success)")
@ActivityStatCard("bell", "Pending", "4", "var(--wa-color-warning)")
@ActivityStatCard("clock-rotate-left", "Last Active", "Just now", "var(--wa-color-neutral-600)")
</div>
@PendingActionsCard()
<div class="content-grid">
@ActiveSessionsCard()
@ConnectedAppsCard()
</div>
<!-- Market Dominance Chart -->
<wa-card style="margin-top: var(--wa-space-l);">
<div slot="header" class="wa-flank">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-m">Market Cap Dominance</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Portfolio allocation by market cap</span>
</div>
</div>
<div style="padding: var(--wa-space-m);">
@components.BubbleChart("market-cap-bubble", marketCapData)
</div>
</wa-card>
}
templ ActivityStatCard(icon string, label string, value string, color string) {
<wa-card class="stat-card">
<div class="wa-flank">
<wa-avatar shape="rounded" style={ "background: " + color + "20;" }>
<wa-icon slot="icon" name={ icon } style={ "color: " + color + ";" }></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-3xs">
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ label }</span>
<span class="wa-heading-2xl">{ value }</span>
</div>
</div>
</wa-card>
}
templ PendingActionsCard() {
<wa-card style="margin-bottom: var(--wa-space-l);">
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Pending Actions</span>
<wa-button appearance="plain" size="small">Clear All</wa-button>
</div>
<div class="wa-stack">
@PendingAction("shield-halved", "var(--wa-color-warning)", "Security Review Required", "New device signed in from San Francisco, CA", "Review", "Dismiss")
<wa-divider></wa-divider>
@PendingAction("signature", "var(--wa-color-primary)", "Signature Request", "DeFi Protocol wants to verify wallet ownership", "Sign", "Reject")
</div>
</wa-card>
}
templ PendingAction(icon string, color string, title string, desc string, primaryAction string, secondaryAction string) {
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: auto;">
<div class="wa-flank wa-gap-m" style="flex: 1;">
<wa-icon class="wa-font-size-l" name={ icon } style={ "color: " + color + ";" }></wa-icon>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ title }</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-600);">{ desc }</span>
</div>
</div>
<div class="wa-cluster wa-gap-xs">
<wa-button variant="brand" size="small">{ primaryAction }</wa-button>
<wa-button variant="neutral" appearance="outlined" size="small">{ secondaryAction }</wa-button>
</div>
</article>
}
templ ActiveSessionsCard() {
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Active Sessions</span>
<wa-button appearance="plain" size="small" variant="danger">Sign Out All</wa-button>
</div>
<div class="wa-stack">
@SessionRow("desktop", "var(--wa-color-primary)", "MacBook Pro", "Chrome 120 · San Francisco, CA", "Active now", true)
<wa-divider></wa-divider>
@SessionRow("mobile", "var(--wa-color-success)", "iPhone 15 Pro", "Safari · San Francisco, CA", "2 hours ago", false)
<wa-divider></wa-divider>
@SessionRow("tablet", "var(--wa-color-warning)", "iPad Air", "Safari · New York, NY", "12 hours ago", false)
</div>
<div slot="footer">
<a href="#" class="wa-cluster wa-gap-xs wa-caption-s" style="color: var(--wa-color-primary);">
<span>View session history</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
}
templ SessionRow(icon string, color string, device string, details string, time string, current bool) {
<article class="wa-flank wa-gap-m">
<wa-icon class="wa-font-size-l" name={ icon } style={ "color: " + color + ";" }></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<div class="wa-cluster wa-gap-xs">
<span class="wa-heading-s">{ device }</span>
if current {
<wa-badge variant="success" pill>Current</wa-badge>
}
</div>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-600);">{ details }</span>
</div>
<span class="wa-caption-xs" style={ sessionTimeStyle(current) }>{ time }</span>
</div>
if !current {
<wa-button variant="danger" appearance="plain" size="small">Revoke</wa-button>
}
</article>
}
func sessionTimeStyle(current bool) string {
if current {
return "color: var(--wa-color-success);"
}
return "color: var(--wa-color-neutral-500);"
}
templ ConnectedAppsCard() {
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Connected Apps</span>
<wa-button appearance="plain" size="small">Manage</wa-button>
</div>
<div class="wa-stack">
@ConnectedAppRow("D", "linear-gradient(135deg, #6366f1, #8b5cf6)", "DeFi Protocol", "defi.example.com", "")
<wa-divider></wa-divider>
@ConnectedAppRow("N", "linear-gradient(135deg, #10b981, #059669)", "NFT Marketplace", "nft.marketplace.io", "transact")
<wa-divider></wa-divider>
@ConnectedAppRow("S", "linear-gradient(135deg, #f59e0b, #d97706)", "Staking Dashboard", "stake.sonr.io", "")
</div>
<div slot="footer">
<a href="/connections" class="wa-cluster wa-gap-xs wa-caption-s" style="color: var(--wa-color-primary);">
<span>Manage all connections</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
}
templ ConnectedAppRow(initials string, bg string, name string, domain string, badge string) {
<article class="wa-flank wa-gap-m">
<wa-avatar initials={ initials } style={ "--size: 36px; background: " + bg + ";" }></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<div class="wa-cluster wa-gap-xs">
<span class="wa-heading-s">{ name }</span>
if badge != "" {
<wa-badge variant="warning" pill>{ badge }</wa-badge>
}
</div>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ domain }</span>
</div>
</div>
<wa-dropdown>
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Actions"></wa-icon-button>
<wa-dropdown-item>View Permissions</wa-dropdown-item>
<wa-divider></wa-divider>
<wa-dropdown-item style="color: var(--wa-color-danger);">Disconnect</wa-dropdown-item>
</wa-dropdown>
</article>
}
templ dashboardStyles() {
<style>
/* Dashboard Content */
#dashboard-content {
padding-top: var(--wa-space-l);
}
.stats-grid { --min-column-size: 160px; margin-bottom: var(--wa-space-l); gap: var(--wa-space-m); }
.stat-card { background: var(--wa-color-surface); }
.quick-actions { display: flex; gap: var(--wa-space-s); }
.token-row { display: flex; align-items: center; padding: var(--wa-space-m) 0; border-bottom: 1px solid var(--wa-color-neutral-100); }
.token-row:last-child { border-bottom: none; }
.token-info { display: flex; align-items: center; gap: var(--wa-space-s); flex: 1; }
.token-balance { text-align: right; }
.token-actions { margin-left: var(--wa-space-m); }
.tx-table { width: 100%; border-collapse: collapse; }
.tx-table th { text-align: left; padding: var(--wa-space-s) var(--wa-space-m); font-size: var(--wa-font-size-xs); font-weight: 500; color: var(--wa-color-neutral-500); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--wa-color-neutral-200); }
.tx-table td { padding: var(--wa-space-m); border-bottom: 1px solid var(--wa-color-neutral-100); font-size: var(--wa-font-size-s); }
.tx-table tr:hover td { background: var(--wa-color-surface-alt); }
.tx-type { display: inline-flex; align-items: center; gap: var(--wa-space-xs); }
.tx-type.send { color: var(--wa-color-danger); }
.tx-type.receive { color: var(--wa-color-success); }
.tx-type.swap { color: var(--wa-color-primary); }
.address-mono { font-family: var(--wa-font-mono); font-size: var(--wa-font-size-xs); }
.filter-bar { display: flex; align-items: center; gap: var(--wa-space-m); margin-bottom: var(--wa-space-l); flex-wrap: wrap; }
.date-group { margin-bottom: var(--wa-space-l); }
.date-group-header { display: flex; align-items: center; gap: var(--wa-space-s); padding: var(--wa-space-s) 0; margin-bottom: var(--wa-space-s); border-bottom: 1px solid var(--wa-color-neutral-200); }
.date-group-header h3 { margin: 0; font-size: var(--wa-font-size-s); font-weight: 600; color: var(--wa-color-neutral-700); }
.tx-count { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
.tx-row { display: grid; grid-template-columns: auto 1fr auto auto auto; gap: var(--wa-space-m); align-items: center; padding: var(--wa-space-m); background: var(--wa-color-surface); border-radius: var(--wa-radius-m); margin-bottom: var(--wa-space-s); transition: box-shadow 0.15s; }
.tx-row:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
.tx-icon { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.tx-icon.send { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.tx-icon.receive { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.tx-icon.swap { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.tx-details { min-width: 0; }
.tx-type-label { font-weight: 500; margin-bottom: 2px; }
.tx-address { font-family: var(--wa-font-mono); font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
.tx-amount { text-align: right; }
.tx-amount .value { font-weight: 500; }
.tx-amount .usd { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
.tx-amount.positive .value { color: var(--wa-color-success); }
.tx-amount.negative .value { color: var(--wa-color-danger); }
.tx-time { text-align: right; min-width: 80px; }
.tx-actions { display: flex; gap: var(--wa-space-2xs); }
.pagination { display: flex; align-items: center; justify-content: space-between; padding: var(--wa-space-m) 0; margin-top: var(--wa-space-l); border-top: 1px solid var(--wa-color-neutral-200); }
.pagination-info { font-size: var(--wa-font-size-s); color: var(--wa-color-neutral-500); }
.pagination-controls { display: flex; align-items: center; gap: var(--wa-space-xs); }
.page-numbers { display: flex; gap: var(--wa-space-2xs); }
.page-btn { min-width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: var(--wa-radius-s); font-size: var(--wa-font-size-s); cursor: pointer; border: 1px solid var(--wa-color-neutral-200); background: var(--wa-color-surface); color: var(--wa-color-neutral-700); transition: all 0.15s; }
.page-btn:hover { background: var(--wa-color-surface-alt); }
.page-btn.active { background: var(--wa-color-primary); border-color: var(--wa-color-primary); color: white; }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.tokens-table { width: 100%; border-collapse: collapse; }
.tokens-table th { text-align: left; padding: var(--wa-space-m) var(--wa-space-l); font-size: var(--wa-font-size-xs); font-weight: 500; color: var(--wa-color-neutral-500); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--wa-color-neutral-200); }
.tokens-table th.sortable { cursor: pointer; user-select: none; }
.tokens-table th.sortable:hover { color: var(--wa-color-neutral-700); }
.tokens-table td { padding: var(--wa-space-m) var(--wa-space-l); border-bottom: 1px solid var(--wa-color-neutral-100); font-size: var(--wa-font-size-s); vertical-align: middle; }
.tokens-table tr:hover td { background: var(--wa-color-surface-alt); }
.token-cell { display: flex; align-items: center; gap: var(--wa-space-s); }
.network-badge { display: inline-flex; align-items: center; gap: var(--wa-space-2xs); padding: var(--wa-space-2xs) var(--wa-space-xs); background: var(--wa-color-surface-alt); border-radius: var(--wa-radius-s); font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-600); }
.table-actions { display: flex; gap: var(--wa-space-xs); }
.change-positive { color: var(--wa-color-success); }
.change-negative { color: var(--wa-color-danger); }
.nft-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--wa-space-l); }
.nft-card { background: var(--wa-color-surface); border-radius: var(--wa-radius-l); overflow: hidden; transition: transform 0.15s, box-shadow 0.15s; cursor: pointer; }
.nft-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); }
.nft-image { position: relative; aspect-ratio: 1; background: var(--wa-color-neutral-100); overflow: hidden; }
.nft-image img { width: 100%; height: 100%; object-fit: cover; }
.nft-badge { position: absolute; top: var(--wa-space-s); right: var(--wa-space-s); }
.nft-actions { position: absolute; bottom: var(--wa-space-s); right: var(--wa-space-s); display: flex; gap: var(--wa-space-2xs); opacity: 0; transition: opacity 0.15s; }
.nft-card:hover .nft-actions { opacity: 1; }
.nft-actions wa-icon-button { background: var(--wa-color-surface); border-radius: 50%; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); }
.nft-info { padding: var(--wa-space-m); }
.nft-collection { display: flex; align-items: center; gap: var(--wa-space-2xs); margin-bottom: var(--wa-space-2xs); }
.nft-collection-name { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
.nft-name { font-weight: 600; margin-bottom: var(--wa-space-s); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nft-price-row { display: flex; justify-content: space-between; align-items: center; }
.nft-floor { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
.nft-value { font-weight: 600; color: var(--wa-color-primary); }
.content-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--wa-space-l); }
@media (max-width: 1024px) { .content-grid { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .tx-row { grid-template-columns: auto 1fr auto; } }
</style>
}