feat(layouts): add app header with blockchain and account selectors
This commit is contained in:
@@ -5,6 +5,66 @@ type WalletUser struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
// Blockchain represents a supported blockchain network
|
||||
type Blockchain struct {
|
||||
ID string
|
||||
Name string
|
||||
Symbol string
|
||||
Icon string
|
||||
Color string
|
||||
Testnet bool
|
||||
}
|
||||
|
||||
// Account represents a wallet account
|
||||
type Account struct {
|
||||
ID string
|
||||
Name string
|
||||
Address string
|
||||
Initials string
|
||||
Color string
|
||||
Active bool
|
||||
}
|
||||
|
||||
// AppContext holds navigation state for the header
|
||||
type AppContext struct {
|
||||
User WalletUser
|
||||
Blockchains []Blockchain
|
||||
Accounts []Account
|
||||
SelectedChainID string
|
||||
SelectedAccountID string
|
||||
}
|
||||
|
||||
// DefaultBlockchains returns the available blockchain networks
|
||||
func DefaultBlockchains() []Blockchain {
|
||||
return []Blockchain{
|
||||
{ID: "sonr", Name: "Sonr", Symbol: "SNR", Icon: "cube", Color: "var(--wa-color-primary)", Testnet: false},
|
||||
{ID: "ethereum", Name: "Ethereum", Symbol: "ETH", Icon: "ethereum", Color: "#627eea", Testnet: false},
|
||||
{ID: "avalanche", Name: "Avalanche", Symbol: "AVAX", Icon: "mountain", Color: "#e84142", Testnet: false},
|
||||
{ID: "polygon", Name: "Polygon", Symbol: "MATIC", Icon: "hexagon", Color: "#8247e5", Testnet: false},
|
||||
{ID: "sonr-testnet", Name: "Sonr Testnet", Symbol: "tSNR", Icon: "cube", Color: "var(--wa-color-warning)", Testnet: true},
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultAccounts returns the user's wallet accounts
|
||||
func DefaultAccounts() []Account {
|
||||
return []Account{
|
||||
{ID: "main", Name: "Main Wallet", Address: "sonr1x9f...7k2m", Initials: "M", Color: "var(--wa-color-primary)", Active: true},
|
||||
{ID: "trading", Name: "Trading", Address: "sonr1k4m...9p3q", Initials: "T", Color: "var(--wa-color-success)", Active: false},
|
||||
{ID: "savings", Name: "Savings", Address: "sonr1r7t...2w8x", Initials: "S", Color: "var(--wa-color-warning)", Active: false},
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultAppContext returns the default navigation context
|
||||
func DefaultAppContext() AppContext {
|
||||
return AppContext{
|
||||
User: WalletUser{Name: "Sonr Wallet", Address: "sonr1x9f...7k2m"},
|
||||
Blockchains: DefaultBlockchains(),
|
||||
Accounts: DefaultAccounts(),
|
||||
SelectedChainID: "sonr",
|
||||
SelectedAccountID: "main",
|
||||
}
|
||||
}
|
||||
|
||||
templ AppLayout(title string, user WalletUser) {
|
||||
@Base(title) {
|
||||
@appStyles()
|
||||
@@ -22,19 +82,71 @@ templ AppLayout(title string, user WalletUser) {
|
||||
}
|
||||
|
||||
templ AppHeader(user WalletUser) {
|
||||
@AppHeaderWithContext(DefaultAppContext())
|
||||
}
|
||||
|
||||
templ AppHeaderWithContext(ctx AppContext) {
|
||||
<div class="header-wrapper">
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<wa-icon name="cube" style="font-size: 24px; color: var(--wa-color-primary);"></wa-icon>
|
||||
<span class="logo-text">Sonr Wallet</span>
|
||||
<wa-breadcrumb>
|
||||
<span slot="separator">/</span>
|
||||
<!-- Sonr Logo -->
|
||||
<wa-breadcrumb-item>
|
||||
<a href="/dashboard" class="breadcrumb-logo">
|
||||
<wa-icon name="cube" style="font-size: 20px; color: var(--wa-color-primary);"></wa-icon>
|
||||
<span>Sonr</span>
|
||||
</a>
|
||||
</wa-breadcrumb-item>
|
||||
<!-- Blockchain Selector -->
|
||||
<wa-breadcrumb-item>
|
||||
<wa-combobox placeholder="Network" value={ ctx.SelectedChainID } size="small" class="nav-combobox">
|
||||
<small>Mainnets</small>
|
||||
for _, chain := range ctx.Blockchains {
|
||||
if !chain.Testnet {
|
||||
<wa-option value={ chain.ID }>
|
||||
<wa-icon slot="prefix" name={ chain.Icon } style={ "color: " + chain.Color + ";" }></wa-icon>
|
||||
{ chain.Name }
|
||||
</wa-option>
|
||||
}
|
||||
}
|
||||
<wa-divider></wa-divider>
|
||||
<small>Testnets</small>
|
||||
for _, chain := range ctx.Blockchains {
|
||||
if chain.Testnet {
|
||||
<wa-option value={ chain.ID }>
|
||||
<wa-icon slot="prefix" name={ chain.Icon } style={ "color: " + chain.Color + ";" }></wa-icon>
|
||||
{ chain.Name }
|
||||
</wa-option>
|
||||
}
|
||||
}
|
||||
</wa-combobox>
|
||||
</wa-breadcrumb-item>
|
||||
<!-- Account Selector -->
|
||||
<wa-breadcrumb-item>
|
||||
<wa-combobox placeholder="Account" value={ ctx.SelectedAccountID } size="small" class="nav-combobox">
|
||||
for _, account := range ctx.Accounts {
|
||||
<wa-option value={ account.ID }>
|
||||
<wa-avatar slot="prefix" initials={ account.Initials } style={ "--size: 20px; background: " + account.Color + ";" }></wa-avatar>
|
||||
{ account.Name }
|
||||
</wa-option>
|
||||
}
|
||||
<wa-divider></wa-divider>
|
||||
<wa-option value="__create__">
|
||||
<wa-icon slot="prefix" name="plus"></wa-icon>
|
||||
Create Account
|
||||
</wa-option>
|
||||
</wa-combobox>
|
||||
</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<wa-dropdown>
|
||||
<wa-avatar slot="trigger" initials="S" style="--size: 36px; background: var(--wa-color-primary); cursor: pointer;"></wa-avatar>
|
||||
<div style="padding: var(--wa-space-m); border-bottom: 1px solid var(--wa-color-neutral-200);">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">{ user.Name }</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ user.Address }</span>
|
||||
<span class="wa-heading-s">{ ctx.User.Name }</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ ctx.User.Address }</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-dropdown-item href="/connections">
|
||||
@@ -95,11 +207,6 @@ templ appStyles() {
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
}
|
||||
.logo-text {
|
||||
font-weight: 600;
|
||||
font-size: var(--wa-font-size-l);
|
||||
color: var(--wa-color-neutral-900);
|
||||
}
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -109,5 +216,39 @@ templ appStyles() {
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--wa-space-l);
|
||||
}
|
||||
/* Breadcrumb Navigation */
|
||||
.header-left wa-breadcrumb {
|
||||
--separator-color: var(--wa-color-neutral-400);
|
||||
}
|
||||
.header-left wa-breadcrumb::part(base) {
|
||||
align-items: center;
|
||||
}
|
||||
.breadcrumb-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-xs);
|
||||
text-decoration: none;
|
||||
color: var(--wa-color-neutral-900);
|
||||
font-weight: 600;
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
.breadcrumb-logo:hover {
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
.nav-combobox {
|
||||
--wa-input-border-color: transparent;
|
||||
--wa-input-background-color: var(--wa-color-surface-alt);
|
||||
min-width: 140px;
|
||||
}
|
||||
.nav-combobox::part(combobox) {
|
||||
border-radius: var(--wa-radius-m);
|
||||
padding: var(--wa-space-2xs) var(--wa-space-s);
|
||||
}
|
||||
.nav-combobox:hover::part(combobox) {
|
||||
background: var(--wa-color-neutral-100);
|
||||
}
|
||||
.nav-combobox::part(display-input) {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ templ Base(title string) {
|
||||
<title>{ title } - Sonr Motr Wallet</title>
|
||||
<script src="https://kit.webawesome.com/47c7425b971f443c.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
|
||||
@@ -43,7 +43,7 @@ func Base(title string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Sonr Motr Wallet</title><script src=\"https://kit.webawesome.com/47c7425b971f443c.js\" crossorigin=\"anonymous\"></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js\"></script><style>\n\t\t\t\t:root {\n\t\t\t\t\t--wa-color-primary: #17c2ff;\n\t\t\t\t}\n\t\t\t\thtml, body {\n\t\t\t\t\tmin-height: 100%;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tcursor: default;\n\t\t\t\t}\n\t\t\t\twa-button, a, [onclick], [hx-get], [hx-post] {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t</style></head><body>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Sonr Motr Wallet</title><script src=\"https://kit.webawesome.com/47c7425b971f443c.js\" crossorigin=\"anonymous\"></script><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha5/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/npm/d3@7\"></script><style>\n\t\t\t\t:root {\n\t\t\t\t\t--wa-color-primary: #17c2ff;\n\t\t\t\t}\n\t\t\t\thtml, body {\n\t\t\t\t\tmin-height: 100%;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tcursor: default;\n\t\t\t\t}\n\t\t\t\twa-button, a, [onclick], [hx-get], [hx-post] {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t</style></head><body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package views
|
||||
|
||||
import "nebula/layouts"
|
||||
import (
|
||||
"nebula/components"
|
||||
"nebula/layouts"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
Symbol string
|
||||
@@ -42,6 +45,8 @@ type DashboardData struct {
|
||||
Tokens []Token
|
||||
Transactions []Transaction
|
||||
NFTs []NFT
|
||||
PortfolioChart []components.AreaSeriesData
|
||||
MarketCapBubble []components.BubbleData
|
||||
}
|
||||
|
||||
func DefaultDashboardData() DashboardData {
|
||||
@@ -63,6 +68,70 @@ func DefaultDashboardData() DashboardData {
|
||||
{Name: "Bored Ape #4521", Collection: "Bored Ape Yacht Club", Image: "https://images.unsplash.com/photo-1620641788421-7a1c342ea42e?w=400&h=400&fit=crop", Floor: "28.5 ETH", Value: "32.0 ETH", Badge: "Listed", Verified: true},
|
||||
{Name: "Sonr Genesis #001", Collection: "Sonr Genesis", Image: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=400&h=400&fit=crop", Floor: "0.8 ETH", Value: "2.5 ETH", Badge: "Rare", Verified: true},
|
||||
},
|
||||
PortfolioChart: DefaultPortfolioChartData(),
|
||||
MarketCapBubble: DefaultMarketCapBubbleData(),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultPortfolioChartData returns sample stacked area data for portfolio performance by asset
|
||||
func DefaultPortfolioChartData() []components.AreaSeriesData {
|
||||
return []components.AreaSeriesData{
|
||||
// Day 1
|
||||
{Date: "2024-12-25", Industry: "ETH", Value: 3200},
|
||||
{Date: "2024-12-25", Industry: "SNR", Value: 2100},
|
||||
{Date: "2024-12-25", Industry: "USDC", Value: 1200},
|
||||
{Date: "2024-12-25", Industry: "AVAX", Value: 500},
|
||||
// Day 2
|
||||
{Date: "2024-12-26", Industry: "ETH", Value: 3350},
|
||||
{Date: "2024-12-26", Industry: "SNR", Value: 2200},
|
||||
{Date: "2024-12-26", Industry: "USDC", Value: 1200},
|
||||
{Date: "2024-12-26", Industry: "AVAX", Value: 520},
|
||||
// Day 3
|
||||
{Date: "2024-12-27", Industry: "ETH", Value: 3500},
|
||||
{Date: "2024-12-27", Industry: "SNR", Value: 2350},
|
||||
{Date: "2024-12-27", Industry: "USDC", Value: 1200},
|
||||
{Date: "2024-12-27", Industry: "AVAX", Value: 480},
|
||||
// Day 4
|
||||
{Date: "2024-12-28", Industry: "ETH", Value: 3280},
|
||||
{Date: "2024-12-28", Industry: "SNR", Value: 2400},
|
||||
{Date: "2024-12-28", Industry: "USDC", Value: 1200},
|
||||
{Date: "2024-12-28", Industry: "AVAX", Value: 510},
|
||||
// Day 5
|
||||
{Date: "2024-12-29", Industry: "ETH", Value: 3450},
|
||||
{Date: "2024-12-29", Industry: "SNR", Value: 2550},
|
||||
{Date: "2024-12-29", Industry: "USDC", Value: 1250},
|
||||
{Date: "2024-12-29", Industry: "AVAX", Value: 530},
|
||||
// Day 6
|
||||
{Date: "2024-12-30", Industry: "ETH", Value: 3600},
|
||||
{Date: "2024-12-30", Industry: "SNR", Value: 2680},
|
||||
{Date: "2024-12-30", Industry: "USDC", Value: 1250},
|
||||
{Date: "2024-12-30", Industry: "AVAX", Value: 550},
|
||||
// Day 7
|
||||
{Date: "2024-12-31", Industry: "ETH", Value: 3750},
|
||||
{Date: "2024-12-31", Industry: "SNR", Value: 2800},
|
||||
{Date: "2024-12-31", Industry: "USDC", Value: 1250},
|
||||
{Date: "2024-12-31", Industry: "AVAX", Value: 580},
|
||||
// Day 8
|
||||
{Date: "2025-01-01", Industry: "ETH", Value: 3680},
|
||||
{Date: "2025-01-01", Industry: "SNR", Value: 2900},
|
||||
{Date: "2025-01-01", Industry: "USDC", Value: 1250},
|
||||
{Date: "2025-01-01", Industry: "AVAX", Value: 600},
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultMarketCapBubbleData returns sample bubble chart data for market cap dominance
|
||||
func DefaultMarketCapBubbleData() []components.BubbleData {
|
||||
return []components.BubbleData{
|
||||
{Name: "BTC", Sector: "Layer 1", Value: 45000},
|
||||
{Name: "ETH", Sector: "Layer 1", Value: 28000},
|
||||
{Name: "SNR", Sector: "Layer 1", Value: 8500},
|
||||
{Name: "SOL", Sector: "Layer 1", Value: 12000},
|
||||
{Name: "AVAX", Sector: "Layer 1", Value: 6000},
|
||||
{Name: "USDC", Sector: "Stablecoin", Value: 15000},
|
||||
{Name: "USDT", Sector: "Stablecoin", Value: 18000},
|
||||
{Name: "UNI", Sector: "DeFi", Value: 4500},
|
||||
{Name: "AAVE", Sector: "DeFi", Value: 3200},
|
||||
{Name: "LINK", Sector: "Oracle", Value: 5800},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +140,9 @@ templ DashboardPage(data DashboardData, activeTab string) {
|
||||
@dashboardStyles()
|
||||
<div class="dashboard-tabs">
|
||||
<wa-tab-group id="dashboard-tabs">
|
||||
<wa-tab panel="accounts" if activeTab == "accounts" || activeTab == "" { active?={ true } }>
|
||||
<wa-icon name="wallet"></wa-icon>
|
||||
Accounts
|
||||
<wa-tab panel="overview" if activeTab == "overview" || activeTab == "" { active?={ true } }>
|
||||
<wa-icon name="grid-2"></wa-icon>
|
||||
Overview
|
||||
</wa-tab>
|
||||
<wa-tab panel="transactions" if activeTab == "transactions" { active?={ true } }>
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
@@ -91,8 +160,8 @@ templ DashboardPage(data DashboardData, activeTab string) {
|
||||
<wa-icon name="chart-line"></wa-icon>
|
||||
Activity
|
||||
</wa-tab>
|
||||
<wa-tab-panel name="accounts">
|
||||
@AccountsPanel(data)
|
||||
<wa-tab-panel name="overview">
|
||||
@OverviewPanel(data)
|
||||
</wa-tab-panel>
|
||||
<wa-tab-panel name="transactions">
|
||||
@TransactionsPanel(data.Transactions)
|
||||
@@ -108,6 +177,9 @@ templ DashboardPage(data DashboardData, activeTab string) {
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
</div>
|
||||
<!-- Drawers -->
|
||||
@components.AllDrawers(components.DefaultTokenOptions(), "sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m")
|
||||
@components.DrawerTriggers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,15 +191,15 @@ templ AccountsPanel(data DashboardData) {
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Manage your crypto assets and balances</span>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">
|
||||
<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">
|
||||
<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">
|
||||
<wa-button variant="brand" size="small" data-drawer-open="swap">
|
||||
<wa-icon slot="start" name="arrow-right-arrow-left"></wa-icon>
|
||||
Swap
|
||||
</wa-button>
|
||||
@@ -163,6 +235,22 @@ templ AccountsPanel(data DashboardData) {
|
||||
</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">
|
||||
@@ -637,6 +725,10 @@ func nftBadgeVariant(badge string) string {
|
||||
}
|
||||
|
||||
templ ActivityPanel() {
|
||||
@ActivityPanelWithData(DefaultMarketCapBubbleData())
|
||||
}
|
||||
|
||||
templ ActivityPanelWithData(marketCapData []components.BubbleData) {
|
||||
<header style="margin-bottom: var(--wa-space-l);">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
@@ -660,6 +752,18 @@ templ ActivityPanel() {
|
||||
@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) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user