feat(layouts): add app layout templates and styles
This commit is contained in:
113
layouts/app.templ
Normal file
113
layouts/app.templ
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
type WalletUser struct {
|
||||||
|
Name string
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AppLayout(title string, user WalletUser) {
|
||||||
|
@Base(title) {
|
||||||
|
@appStyles()
|
||||||
|
<wa-page>
|
||||||
|
<div class="app-layout">
|
||||||
|
@AppHeader(user)
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="content-container">
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</wa-page>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AppHeader(user WalletUser) {
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<wa-dropdown-item href="/connections">
|
||||||
|
<wa-icon slot="icon" name="plug"></wa-icon>
|
||||||
|
Connections
|
||||||
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item href="/devices">
|
||||||
|
<wa-icon slot="icon" name="mobile"></wa-icon>
|
||||||
|
Devices
|
||||||
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item href="/services">
|
||||||
|
<wa-icon slot="icon" name="server"></wa-icon>
|
||||||
|
Services
|
||||||
|
</wa-dropdown-item>
|
||||||
|
<wa-dropdown-item href="/settings">
|
||||||
|
<wa-icon slot="icon" name="gear"></wa-icon>
|
||||||
|
Settings
|
||||||
|
</wa-dropdown-item>
|
||||||
|
<wa-divider></wa-divider>
|
||||||
|
<wa-dropdown-item variant="danger" href="/logout">
|
||||||
|
<wa-icon slot="icon" name="arrow-right-from-bracket"></wa-icon>
|
||||||
|
Sign Out
|
||||||
|
</wa-dropdown-item>
|
||||||
|
</wa-dropdown>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ appStyles() {
|
||||||
|
<style>
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--wa-color-surface-alt);
|
||||||
|
}
|
||||||
|
.header-wrapper {
|
||||||
|
background: var(--wa-color-surface);
|
||||||
|
border-bottom: 1px solid var(--wa-color-neutral-200);
|
||||||
|
}
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--wa-space-s) var(--wa-space-l);
|
||||||
|
background: var(--wa-color-surface);
|
||||||
|
}
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--wa-space-m);
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.content-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--wa-space-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
167
layouts/app_templ.go
Normal file
167
layouts/app_templ.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package layouts
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type WalletUser struct {
|
||||||
|
Name string
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppLayout(title string, user WalletUser) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = appStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <wa-page><div class=\"app-layout\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = AppHeader(user).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"main-content\"><div class=\"content-container\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></main></div></wa-page>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Base(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppHeader(user WalletUser) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var3 == nil {
|
||||||
|
templ_7745c5c3_Var3 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<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></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\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/app.templ`, Line: 36, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> <span class=\"wa-caption-xs\" style=\"color: var(--wa-color-neutral-500);\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Address)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/app.templ`, Line: 37, Col: 93}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></div></div><wa-dropdown-item href=\"/connections\"><wa-icon slot=\"icon\" name=\"plug\"></wa-icon> Connections</wa-dropdown-item> <wa-dropdown-item href=\"/devices\"><wa-icon slot=\"icon\" name=\"mobile\"></wa-icon> Devices</wa-dropdown-item> <wa-dropdown-item href=\"/services\"><wa-icon slot=\"icon\" name=\"server\"></wa-icon> Services</wa-dropdown-item> <wa-dropdown-item href=\"/settings\"><wa-icon slot=\"icon\" name=\"gear\"></wa-icon> Settings</wa-dropdown-item> <wa-divider></wa-divider> <wa-dropdown-item variant=\"danger\" href=\"/logout\"><wa-icon slot=\"icon\" name=\"arrow-right-from-bracket\"></wa-icon> Sign Out</wa-dropdown-item></wa-dropdown></div></header></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func appStyles() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var6 == nil {
|
||||||
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<style>\n\t\t.app-layout {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tmin-height: 100vh;\n\t\t\tbackground: var(--wa-color-surface-alt);\n\t\t}\n\t\t.header-wrapper {\n\t\t\tbackground: var(--wa-color-surface);\n\t\t\tborder-bottom: 1px solid var(--wa-color-neutral-200);\n\t\t}\n\t\t.app-header {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: space-between;\n\t\t\tmax-width: 1200px;\n\t\t\tmargin: 0 auto;\n\t\t\tpadding: var(--wa-space-s) var(--wa-space-l);\n\t\t\tbackground: var(--wa-color-surface);\n\t\t}\n\t\t.header-left {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: var(--wa-space-m);\n\t\t}\n\t\t.header-right {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: var(--wa-space-m);\n\t\t}\n\t\t.logo-text {\n\t\t\tfont-weight: 600;\n\t\t\tfont-size: var(--wa-font-size-l);\n\t\t\tcolor: var(--wa-color-neutral-900);\n\t\t}\n\t\t.main-content {\n\t\t\tflex: 1;\n\t\t\toverflow-y: auto;\n\t\t}\n\t\t.content-container {\n\t\t\tmax-width: 1200px;\n\t\t\tmargin: 0 auto;\n\t\t\tpadding: 0 var(--wa-space-l);\n\t\t}\n\t</style>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
600
views/authorize.templ
Normal file
600
views/authorize.templ
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import "nebula/layouts"
|
||||||
|
|
||||||
|
type AppInfo struct {
|
||||||
|
Name string
|
||||||
|
Domain string
|
||||||
|
LogoIcon string
|
||||||
|
Verified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletInfo struct {
|
||||||
|
Name string
|
||||||
|
Address string
|
||||||
|
Balance string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenAmount struct {
|
||||||
|
Symbol string
|
||||||
|
Amount string
|
||||||
|
USD string
|
||||||
|
Initials string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxDetails struct {
|
||||||
|
Type string
|
||||||
|
FromToken TokenAmount
|
||||||
|
ToToken TokenAmount
|
||||||
|
Network string
|
||||||
|
NetworkFee string
|
||||||
|
MaxFee string
|
||||||
|
Slippage string
|
||||||
|
Contract string
|
||||||
|
Function string
|
||||||
|
RawData string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthRequest struct {
|
||||||
|
Type string
|
||||||
|
App AppInfo
|
||||||
|
Wallet WalletInfo
|
||||||
|
Message string
|
||||||
|
MessageHex string
|
||||||
|
Transaction *TxDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthorizePage(req AuthRequest) {
|
||||||
|
@layouts.CenteredCard("Authorize - Sonr") {
|
||||||
|
<div id="auth-content">
|
||||||
|
@AuthorizeContent(req)
|
||||||
|
</div>
|
||||||
|
<div id="htmx-indicator" class="htmx-indicator">
|
||||||
|
<wa-spinner></wa-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthorizeContent(req AuthRequest) {
|
||||||
|
@authorizeStyles()
|
||||||
|
@AppIdentityHeader(req.App)
|
||||||
|
<wa-divider></wa-divider>
|
||||||
|
@RequestTypeTabs(req)
|
||||||
|
<footer slot="footer">
|
||||||
|
@AuthFooterActions(req.Type)
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AppIdentityHeader(app AppInfo) {
|
||||||
|
<div class="app-identity">
|
||||||
|
<div class="app-logo">
|
||||||
|
<wa-icon name={ app.LogoIcon } style="font-size: 32px; color: var(--wa-color-primary);"></wa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="wa-stack wa-gap-2xs" style="align-items: center;">
|
||||||
|
<div class="app-name">
|
||||||
|
<span class="wa-heading-l">{ app.Name }</span>
|
||||||
|
if app.Verified {
|
||||||
|
<wa-icon name="badge-check" variant="solid" class="verified-badge"></wa-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ app.Domain }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ RequestTypeTabs(req AuthRequest) {
|
||||||
|
<wa-tab-group class="request-tabs" id="auth-tabs">
|
||||||
|
<wa-tab panel="connect" if req.Type == "connect" || req.Type == "" {
|
||||||
|
active?={ true }
|
||||||
|
}>
|
||||||
|
<div class="wa-cluster wa-gap-xs">
|
||||||
|
<wa-icon name="link"></wa-icon>
|
||||||
|
<span>Connect</span>
|
||||||
|
</div>
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab panel="sign" if req.Type == "sign" {
|
||||||
|
active?={ true }
|
||||||
|
}>
|
||||||
|
<div class="wa-cluster wa-gap-xs">
|
||||||
|
<wa-icon name="pen-nib"></wa-icon>
|
||||||
|
<span>Sign</span>
|
||||||
|
</div>
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab panel="transaction" if req.Type == "transaction" {
|
||||||
|
active?={ true }
|
||||||
|
}>
|
||||||
|
<div class="wa-cluster wa-gap-xs">
|
||||||
|
<wa-icon name="paper-plane"></wa-icon>
|
||||||
|
<span>Transaction</span>
|
||||||
|
</div>
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab-panel name="connect">
|
||||||
|
@ConnectPanel(req.Wallet)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="sign">
|
||||||
|
@SignPanel(req.Wallet, req.Message, req.MessageHex)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="transaction">
|
||||||
|
@TransactionPanel(req.Wallet, req.Transaction)
|
||||||
|
</wa-tab-panel>
|
||||||
|
</wa-tab-group>
|
||||||
|
@tabScripts()
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ConnectPanel(wallet WalletInfo) {
|
||||||
|
<div class="wa-stack wa-gap-m">
|
||||||
|
@WalletSelector(wallet, false, "")
|
||||||
|
<div class="wa-stack wa-gap-xs">
|
||||||
|
<span class="wa-heading-s">This app wants to:</span>
|
||||||
|
@PermissionItem("eye", "read", "See your wallet address", "Your public address only")
|
||||||
|
@PermissionItem("coins", "read", "See your balances", "Token amounts in your wallet")
|
||||||
|
@PermissionItem("bell", "write", "Ask to send transactions", "You'll approve each one separately")
|
||||||
|
</div>
|
||||||
|
<wa-callout variant="neutral">
|
||||||
|
<wa-icon slot="icon" name="shield-check"></wa-icon>
|
||||||
|
<div class="wa-stack wa-gap-2xs">
|
||||||
|
<span class="wa-heading-xs">This app cannot:</span>
|
||||||
|
<span class="wa-caption-s">Spend your funds without asking you first.</span>
|
||||||
|
</div>
|
||||||
|
</wa-callout>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ SignPanel(wallet WalletInfo, message string, messageHex string) {
|
||||||
|
<div class="wa-stack wa-gap-m">
|
||||||
|
@WalletSelector(wallet, true, "Signing")
|
||||||
|
<div class="wa-stack wa-gap-xs">
|
||||||
|
<span class="wa-heading-s">You're signing this message:</span>
|
||||||
|
<div class="tx-preview">{ message }</div>
|
||||||
|
</div>
|
||||||
|
<wa-callout variant="brand" appearance="filled">
|
||||||
|
<wa-icon slot="icon" name="signature"></wa-icon>
|
||||||
|
<div class="wa-stack wa-gap-2xs">
|
||||||
|
<span class="wa-heading-xs">Free to sign</span>
|
||||||
|
<span class="wa-caption-s">No fees. This just proves you own this wallet.</span>
|
||||||
|
</div>
|
||||||
|
</wa-callout>
|
||||||
|
if messageHex != "" {
|
||||||
|
<div class="raw-data-section">
|
||||||
|
<wa-details summary="Show technical details">
|
||||||
|
<div class="raw-data-content">{ messageHex }</div>
|
||||||
|
</wa-details>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TransactionPanel(wallet WalletInfo, tx *TxDetails) {
|
||||||
|
<div class="wa-stack wa-gap-m">
|
||||||
|
@WalletSelectorWithBalance(wallet)
|
||||||
|
if tx != nil {
|
||||||
|
@TransactionCard(tx)
|
||||||
|
@TransactionDetails(tx)
|
||||||
|
<wa-callout variant="warning">
|
||||||
|
<wa-icon slot="icon" name="triangle-alert"></wa-icon>
|
||||||
|
<div class="wa-stack wa-gap-2xs">
|
||||||
|
<span class="wa-heading-xs">Large amount</span>
|
||||||
|
<span class="wa-caption-s">Please review the details carefully before confirming.</span>
|
||||||
|
</div>
|
||||||
|
</wa-callout>
|
||||||
|
<div class="raw-data-section">
|
||||||
|
<wa-details summary="Show technical details">
|
||||||
|
<div class="wa-stack wa-gap-s">
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Contract</span>
|
||||||
|
<span class="tx-value">{ tx.Contract }</span>
|
||||||
|
</div>
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Function</span>
|
||||||
|
<span class="tx-value">{ tx.Function }</span>
|
||||||
|
</div>
|
||||||
|
<div class="raw-data-content">{ tx.RawData }</div>
|
||||||
|
</div>
|
||||||
|
</wa-details>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ WalletSelector(wallet WalletInfo, showBadge bool, badgeText string) {
|
||||||
|
<div class="wallet-selector">
|
||||||
|
<div class="wa-flank">
|
||||||
|
<div class="wa-cluster wa-gap-s">
|
||||||
|
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
|
||||||
|
<div class="wa-stack wa-gap-0">
|
||||||
|
<span class="wa-heading-s">{ wallet.Name }</span>
|
||||||
|
<span class="wallet-address">{ wallet.Address }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if showBadge && badgeText != "" {
|
||||||
|
<wa-badge variant="neutral">{ badgeText }</wa-badge>
|
||||||
|
} else {
|
||||||
|
<wa-icon-button name="chevron-down" variant="plain"></wa-icon-button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ WalletSelectorWithBalance(wallet WalletInfo) {
|
||||||
|
<div class="wallet-selector">
|
||||||
|
<div class="wa-flank">
|
||||||
|
<div class="wa-cluster wa-gap-s">
|
||||||
|
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
|
||||||
|
<div class="wa-stack wa-gap-0">
|
||||||
|
<span class="wa-heading-s">{ wallet.Name }</span>
|
||||||
|
<span class="wallet-address">{ wallet.Address }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wa-stack wa-gap-0" style="text-align: right;">
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Balance</span>
|
||||||
|
<span class="wa-heading-s">{ wallet.Balance }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ PermissionItem(icon string, variant string, title string, description string) {
|
||||||
|
<div class="permission-item">
|
||||||
|
<div class={ "permission-icon", variant }>
|
||||||
|
<wa-icon name={ icon }></wa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="wa-stack wa-gap-0">
|
||||||
|
<span class="wa-heading-xs">{ title }</span>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ description }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TransactionCard(tx *TxDetails) {
|
||||||
|
<wa-card>
|
||||||
|
<div class="wa-stack wa-gap-s">
|
||||||
|
<div class="wa-flank">
|
||||||
|
<span class="wa-heading-m">{ tx.Type }</span>
|
||||||
|
<wa-badge variant="warning">Waiting</wa-badge>
|
||||||
|
</div>
|
||||||
|
<wa-divider></wa-divider>
|
||||||
|
<div class="wa-stack wa-gap-s">
|
||||||
|
<div class="wa-flank">
|
||||||
|
<div class="wa-cluster wa-gap-s">
|
||||||
|
<wa-avatar initials={ tx.FromToken.Initials } style="--size: 32px; background: var(--wa-color-neutral-200);"></wa-avatar>
|
||||||
|
<div class="wa-stack wa-gap-0">
|
||||||
|
<span class="wa-heading-s">{ tx.FromToken.Amount } { tx.FromToken.Symbol }</span>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ tx.FromToken.Symbol }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-danger);">-{ tx.FromToken.USD }</span>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<wa-icon name="arrow-down" style="color: var(--wa-color-neutral-400);"></wa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="wa-flank">
|
||||||
|
<div class="wa-cluster wa-gap-s">
|
||||||
|
<wa-avatar initials={ tx.ToToken.Initials } style="--size: 32px; background: var(--wa-color-primary-subtle);"></wa-avatar>
|
||||||
|
<div class="wa-stack wa-gap-0">
|
||||||
|
<span class="wa-heading-s">~{ tx.ToToken.Amount } { tx.ToToken.Symbol }</span>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ tx.ToToken.Symbol }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-success);">+{ tx.ToToken.USD }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</wa-card>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TransactionDetails(tx *TxDetails) {
|
||||||
|
<div class="wa-stack wa-gap-2xs">
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Network</span>
|
||||||
|
<span class="tx-value">{ tx.Network }</span>
|
||||||
|
</div>
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Network fee</span>
|
||||||
|
<span class="tx-value">{ tx.NetworkFee }</span>
|
||||||
|
</div>
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Max fee</span>
|
||||||
|
<span class="tx-value">{ tx.MaxFee }</span>
|
||||||
|
</div>
|
||||||
|
<div class="tx-detail-row">
|
||||||
|
<span class="tx-label">Price change limit</span>
|
||||||
|
<span class="tx-value">{ tx.Slippage }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthFooterActions(requestType string) {
|
||||||
|
<div class="wa-stack wa-gap-s">
|
||||||
|
<div class="auth-actions">
|
||||||
|
<wa-button
|
||||||
|
variant="neutral"
|
||||||
|
appearance="outlined"
|
||||||
|
id="deny-btn"
|
||||||
|
hx-post="/authorize/deny"
|
||||||
|
hx-target="#auth-content"
|
||||||
|
hx-swap="innerHTML transition:true"
|
||||||
|
hx-indicator="#htmx-indicator"
|
||||||
|
>
|
||||||
|
<wa-icon slot="start" name="x"></wa-icon>
|
||||||
|
Cancel
|
||||||
|
</wa-button>
|
||||||
|
<wa-button
|
||||||
|
variant="brand"
|
||||||
|
id="approve-btn"
|
||||||
|
hx-post="/authorize/approve"
|
||||||
|
hx-target="#auth-content"
|
||||||
|
hx-swap="innerHTML transition:true"
|
||||||
|
hx-indicator="#htmx-indicator"
|
||||||
|
hx-vals={ `{"type":"` + requestType + `"}` }
|
||||||
|
>
|
||||||
|
<wa-icon slot="start" name="check"></wa-icon>
|
||||||
|
if requestType == "sign" {
|
||||||
|
Sign
|
||||||
|
} else if requestType == "transaction" {
|
||||||
|
Confirm
|
||||||
|
} else {
|
||||||
|
Allow
|
||||||
|
}
|
||||||
|
</wa-button>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||||
|
<wa-icon name="lock" style="font-size: 12px;"></wa-icon>
|
||||||
|
Protected by Sonr
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthResultSuccess(actionType string) {
|
||||||
|
@authorizeStyles()
|
||||||
|
<div class="wa-stack wa-gap-l" style="text-align: center; padding: var(--wa-space-xl) 0;">
|
||||||
|
<wa-icon name="circle-check" variant="solid" style="font-size: 64px; color: var(--wa-color-success);"></wa-icon>
|
||||||
|
<span class="wa-heading-l">
|
||||||
|
if actionType == "sign" {
|
||||||
|
Signed
|
||||||
|
} else if actionType == "transaction" {
|
||||||
|
Sent
|
||||||
|
} else {
|
||||||
|
Connected
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
|
||||||
|
if actionType == "transaction" {
|
||||||
|
Transaction ID: 0x8f4a2b1c...
|
||||||
|
} else {
|
||||||
|
You can close this window.
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
|
||||||
|
Close
|
||||||
|
</wa-button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AuthResultDenied() {
|
||||||
|
@authorizeStyles()
|
||||||
|
<div class="wa-stack wa-gap-l" style="text-align: center; padding: var(--wa-space-xl) 0;">
|
||||||
|
<wa-icon name="circle-x" variant="solid" style="font-size: 64px; color: var(--wa-color-danger);"></wa-icon>
|
||||||
|
<span class="wa-heading-l">Cancelled</span>
|
||||||
|
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
|
||||||
|
No access was granted.
|
||||||
|
</span>
|
||||||
|
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
|
||||||
|
Close
|
||||||
|
</wa-button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ tabScripts() {
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const tabGroup = document.getElementById('auth-tabs');
|
||||||
|
const approveBtn = document.getElementById('approve-btn');
|
||||||
|
|
||||||
|
if (tabGroup && approveBtn) {
|
||||||
|
tabGroup.addEventListener('wa-tab-show', (event) => {
|
||||||
|
const panel = event.detail.name;
|
||||||
|
let icon, text;
|
||||||
|
|
||||||
|
switch (panel) {
|
||||||
|
case 'connect':
|
||||||
|
icon = 'link';
|
||||||
|
text = 'Allow';
|
||||||
|
break;
|
||||||
|
case 'sign':
|
||||||
|
icon = 'pen-nib';
|
||||||
|
text = 'Sign';
|
||||||
|
break;
|
||||||
|
case 'transaction':
|
||||||
|
icon = 'paper-plane';
|
||||||
|
text = 'Confirm';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
approveBtn.innerHTML = `<wa-icon slot="start" name="${icon}"></wa-icon>${text}`;
|
||||||
|
approveBtn.setAttribute('hx-vals', JSON.stringify({type: panel}));
|
||||||
|
htmx.process(approveBtn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ authorizeStyles() {
|
||||||
|
<style>
|
||||||
|
.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.htmx-request .htmx-indicator {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.htmx-request #auth-content {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
@view-transition {
|
||||||
|
navigation: auto;
|
||||||
|
}
|
||||||
|
.app-identity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--wa-space-s);
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: var(--wa-space-m);
|
||||||
|
}
|
||||||
|
.app-logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: var(--wa-radius-l);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--wa-color-surface-alt);
|
||||||
|
border: 1px solid var(--wa-color-neutral-200);
|
||||||
|
}
|
||||||
|
.app-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--wa-space-2xs);
|
||||||
|
}
|
||||||
|
.verified-badge {
|
||||||
|
color: var(--wa-color-primary);
|
||||||
|
}
|
||||||
|
.wallet-selector {
|
||||||
|
background: var(--wa-color-surface-alt);
|
||||||
|
border-radius: var(--wa-radius-m);
|
||||||
|
padding: var(--wa-space-s) var(--wa-space-m);
|
||||||
|
}
|
||||||
|
.wallet-address {
|
||||||
|
font-family: var(--wa-font-mono);
|
||||||
|
font-size: var(--wa-font-size-s);
|
||||||
|
color: var(--wa-color-neutral-500);
|
||||||
|
}
|
||||||
|
.permission-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--wa-space-s);
|
||||||
|
padding: var(--wa-space-s) 0;
|
||||||
|
}
|
||||||
|
.permission-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--wa-radius-s);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.permission-icon.read {
|
||||||
|
background: var(--wa-color-success-subtle);
|
||||||
|
color: var(--wa-color-success);
|
||||||
|
}
|
||||||
|
.permission-icon.write {
|
||||||
|
background: var(--wa-color-warning-subtle);
|
||||||
|
color: var(--wa-color-warning);
|
||||||
|
}
|
||||||
|
.permission-icon.sign {
|
||||||
|
background: var(--wa-color-primary-subtle);
|
||||||
|
color: var(--wa-color-primary);
|
||||||
|
}
|
||||||
|
.permission-icon.danger {
|
||||||
|
background: var(--wa-color-danger-subtle);
|
||||||
|
color: var(--wa-color-danger);
|
||||||
|
}
|
||||||
|
.tx-preview {
|
||||||
|
background: var(--wa-color-surface-alt);
|
||||||
|
border-radius: var(--wa-radius-m);
|
||||||
|
padding: var(--wa-space-m);
|
||||||
|
font-family: var(--wa-font-mono);
|
||||||
|
font-size: var(--wa-font-size-s);
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.tx-detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--wa-space-xs) 0;
|
||||||
|
}
|
||||||
|
.tx-detail-row:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--wa-color-neutral-200);
|
||||||
|
}
|
||||||
|
.tx-label {
|
||||||
|
color: var(--wa-color-neutral-600);
|
||||||
|
}
|
||||||
|
.tx-value {
|
||||||
|
font-family: var(--wa-font-mono);
|
||||||
|
font-size: var(--wa-font-size-s);
|
||||||
|
}
|
||||||
|
.auth-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--wa-space-s);
|
||||||
|
}
|
||||||
|
.auth-actions wa-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.request-tabs wa-tab-panel {
|
||||||
|
padding-top: var(--wa-space-m);
|
||||||
|
}
|
||||||
|
.raw-data-section {
|
||||||
|
margin-top: var(--wa-space-m);
|
||||||
|
}
|
||||||
|
.raw-data-content {
|
||||||
|
background: var(--wa-color-neutral-900);
|
||||||
|
color: var(--wa-color-neutral-100);
|
||||||
|
border-radius: var(--wa-radius-m);
|
||||||
|
padding: var(--wa-space-m);
|
||||||
|
font-family: var(--wa-font-mono);
|
||||||
|
font-size: var(--wa-font-size-xs);
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: var(--wa-space-s);
|
||||||
|
}
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.app-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
.permission-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.wallet-selector {
|
||||||
|
padding: var(--wa-space-xs) var(--wa-space-s);
|
||||||
|
}
|
||||||
|
.tx-preview {
|
||||||
|
padding: var(--wa-space-s);
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.raw-data-content {
|
||||||
|
padding: var(--wa-space-s);
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.app-identity {
|
||||||
|
padding-bottom: var(--wa-space-s);
|
||||||
|
}
|
||||||
|
.app-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
.permission-item {
|
||||||
|
padding: var(--wa-space-xs) 0;
|
||||||
|
}
|
||||||
|
.tx-preview {
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
1269
views/authorize_templ.go
Normal file
1269
views/authorize_templ.go
Normal file
File diff suppressed because it is too large
Load Diff
885
views/dashboard.templ
Normal file
885
views/dashboard.templ
Normal file
@@ -0,0 +1,885 @@
|
|||||||
|
package views
|
||||||
|
|
||||||
|
import "nebula/layouts"
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Symbol string
|
||||||
|
Name string
|
||||||
|
Balance string
|
||||||
|
Value string
|
||||||
|
Change string
|
||||||
|
Positive bool
|
||||||
|
Color string
|
||||||
|
Initials string
|
||||||
|
Network string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
Type string
|
||||||
|
Asset string
|
||||||
|
Amount string
|
||||||
|
USD string
|
||||||
|
Date string
|
||||||
|
Time string
|
||||||
|
Hash string
|
||||||
|
Positive bool
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NFT struct {
|
||||||
|
Name string
|
||||||
|
Collection string
|
||||||
|
Image string
|
||||||
|
Floor string
|
||||||
|
Value string
|
||||||
|
Badge string
|
||||||
|
Verified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardData struct {
|
||||||
|
TotalBalance string
|
||||||
|
Change24h string
|
||||||
|
Tokens []Token
|
||||||
|
Transactions []Transaction
|
||||||
|
NFTs []NFT
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultDashboardData() DashboardData {
|
||||||
|
return DashboardData{
|
||||||
|
TotalBalance: "12847.32",
|
||||||
|
Change24h: "+$302.18",
|
||||||
|
Tokens: []Token{
|
||||||
|
{Symbol: "SNR", Name: "Sonr", Balance: "8,432.50", Value: "$4,216.25", Change: "+5.67%", Positive: true, Color: "linear-gradient(135deg, #17c2ff, #0090ff)", Initials: "S", Network: "Sonr Mainnet"},
|
||||||
|
{Symbol: "ETH", Name: "Ethereum", Balance: "2.847", Value: "$6,682.04", Change: "+3.24%", Positive: true, Color: "#627eea", Initials: "E", Network: "Ethereum"},
|
||||||
|
{Symbol: "USDC", Name: "USD Coin", Balance: "1,250.00", Value: "$1,250.00", Change: "0.00%", Positive: true, Color: "#2775ca", Initials: "U", Network: "Ethereum"},
|
||||||
|
{Symbol: "AVAX", Name: "Avalanche", Balance: "24.83", Value: "$699.03", Change: "-2.18%", Positive: false, Color: "#e84142", Initials: "A", Network: "Avalanche"},
|
||||||
|
},
|
||||||
|
Transactions: []Transaction{
|
||||||
|
{Type: "receive", Asset: "ETH", Amount: "+0.25 ETH", USD: "$586.25", Date: "Today", Time: "2:34 PM", Hash: "0x8f4a2b1c...", Positive: true, Address: "0x742d...35Cb"},
|
||||||
|
{Type: "send", Asset: "SNR", Amount: "-500 SNR", USD: "$250.00", Date: "Yesterday", Time: "11:22 AM", Hash: "0x7d3e2a1b...", Positive: false, Address: "sonr1k4m...9p3q"},
|
||||||
|
{Type: "swap", Asset: "ETH → USDC", Amount: "0.5 ETH", USD: "→ 1,172.50 USDC", Date: "Jan 1", Time: "9:15 AM", Hash: "0x1a2b3c4d...", Positive: true, Address: "Uniswap V3"},
|
||||||
|
},
|
||||||
|
NFTs: []NFT{
|
||||||
|
{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},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DashboardPage(data DashboardData, activeTab string) {
|
||||||
|
@layouts.AppLayout("Dashboard - Sonr", layouts.WalletUser{Name: "Sonr Wallet", Address: "sonr1x9f...7k2m"}) {
|
||||||
|
@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>
|
||||||
|
<wa-tab panel="transactions" if activeTab == "transactions" { active?={ true } }>
|
||||||
|
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||||
|
Transactions
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab panel="tokens" if activeTab == "tokens" { active?={ true } }>
|
||||||
|
<wa-icon name="coins"></wa-icon>
|
||||||
|
Tokens
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab panel="nfts" if activeTab == "nfts" { active?={ true } }>
|
||||||
|
<wa-icon name="image"></wa-icon>
|
||||||
|
NFTs
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab panel="activity" if activeTab == "activity" { active?={ true } }>
|
||||||
|
<wa-icon name="chart-line"></wa-icon>
|
||||||
|
Activity
|
||||||
|
</wa-tab>
|
||||||
|
<wa-tab-panel name="accounts">
|
||||||
|
@AccountsPanel(data)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="transactions">
|
||||||
|
@TransactionsPanel(data.Transactions)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="tokens">
|
||||||
|
@TokensPanel(data.Tokens)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="nfts">
|
||||||
|
@NFTsPanel(data.NFTs)
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="activity">
|
||||||
|
@ActivityPanel()
|
||||||
|
</wa-tab-panel>
|
||||||
|
</wa-tab-group>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ AccountsPanel(data 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">Accounts</span>
|
||||||
|
<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-icon slot="start" name="arrow-down"></wa-icon>
|
||||||
|
Receive
|
||||||
|
</wa-button>
|
||||||
|
<wa-button variant="neutral" appearance="outlined" size="small">
|
||||||
|
<wa-icon slot="start" name="arrow-up"></wa-icon>
|
||||||
|
Send
|
||||||
|
</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-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>
|
||||||
|
<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 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 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 []Transaction) {
|
||||||
|
<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>
|
||||||
|
<div class="wa-grid stats-grid">
|
||||||
|
@StatCard("Total Received", "+$15,420.50", "42 transactions", "success")
|
||||||
|
@StatCard("Total Sent", "-$8,234.18", "28 transactions", "danger")
|
||||||
|
@StatCard("Swaps", "12", "$4,892.00 volume", "neutral")
|
||||||
|
@StatCard("Gas Spent", "$127.45", "All time", "neutral")
|
||||||
|
</div>
|
||||||
|
<wa-card>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<wa-select placeholder="All Types" size="small" style="min-width: 140px;" clearable>
|
||||||
|
<wa-option value="send">Send</wa-option>
|
||||||
|
<wa-option value="receive">Receive</wa-option>
|
||||||
|
<wa-option value="swap">Swap</wa-option>
|
||||||
|
</wa-select>
|
||||||
|
<wa-select placeholder="All Assets" size="small" style="min-width: 140px;" clearable>
|
||||||
|
<wa-option value="eth">ETH</wa-option>
|
||||||
|
<wa-option value="snr">SNR</wa-option>
|
||||||
|
<wa-option value="usdc">USDC</wa-option>
|
||||||
|
</wa-select>
|
||||||
|
<div style="flex: 1;"></div>
|
||||||
|
<wa-input placeholder="Search by hash or address..." size="small" style="width: 240px;">
|
||||||
|
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||||
|
</wa-input>
|
||||||
|
</div>
|
||||||
|
@TransactionDateGroup("Today", transactions[:1])
|
||||||
|
@TransactionDateGroup("Yesterday", transactions[1:])
|
||||||
|
@Pagination(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 TransactionDateGroup(date string, transactions []Transaction) {
|
||||||
|
<div class="date-group">
|
||||||
|
<div class="date-group-header">
|
||||||
|
<h3>{ date }</h3>
|
||||||
|
<span class="tx-count">{ lenStr(transactions) } transaction(s)</span>
|
||||||
|
</div>
|
||||||
|
for _, tx := range transactions {
|
||||||
|
@TransactionRow(tx)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
func lenStr(txs []Transaction) string {
|
||||||
|
return string(rune('0' + len(txs)))
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TransactionRow(tx Transaction) {
|
||||||
|
<div class="tx-row">
|
||||||
|
<div class={ "tx-icon", tx.Type }>
|
||||||
|
if tx.Type == "receive" {
|
||||||
|
<wa-icon name="arrow-down"></wa-icon>
|
||||||
|
} else if tx.Type == "send" {
|
||||||
|
<wa-icon name="arrow-up"></wa-icon>
|
||||||
|
} else {
|
||||||
|
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="tx-details">
|
||||||
|
<div class="tx-type-label">
|
||||||
|
if tx.Type == "receive" {
|
||||||
|
Received { tx.Asset }
|
||||||
|
} else if tx.Type == "send" {
|
||||||
|
Sent { tx.Asset }
|
||||||
|
} else {
|
||||||
|
Swapped { tx.Asset }
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="tx-address">{ tx.Address }</div>
|
||||||
|
</div>
|
||||||
|
<div class={ "tx-amount", templ.KV("positive", tx.Positive), templ.KV("negative", !tx.Positive) }>
|
||||||
|
<div class="value">{ tx.Amount }</div>
|
||||||
|
<div class="usd">{ tx.USD }</div>
|
||||||
|
</div>
|
||||||
|
<div class="tx-time">
|
||||||
|
<div class="time">{ tx.Time }</div>
|
||||||
|
<wa-badge variant="success" size="small">Confirmed</wa-badge>
|
||||||
|
</div>
|
||||||
|
<div class="tx-actions">
|
||||||
|
<wa-tooltip content="View on Explorer">
|
||||||
|
<wa-icon-button name="arrow-up-right-from-square" label="Explorer"></wa-icon-button>
|
||||||
|
</wa-tooltip>
|
||||||
|
<wa-copy-button value={ tx.Hash } copy-label="Copy hash"></wa-copy-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Pagination(current int, total int, perPage int) {
|
||||||
|
<div class="pagination">
|
||||||
|
<div class="pagination-info">
|
||||||
|
Showing <strong>1-{ string(rune('0' + perPage)) }</strong> of <strong>{ string(rune('0' + total/10)) }{ string(rune('0' + total%10)) }</strong> transactions
|
||||||
|
</div>
|
||||||
|
<div class="pagination-controls">
|
||||||
|
<wa-select value="10" size="small" style="width: 100px;">
|
||||||
|
<wa-option value="10">10 / page</wa-option>
|
||||||
|
<wa-option value="25">25 / page</wa-option>
|
||||||
|
<wa-option value="50">50 / page</wa-option>
|
||||||
|
</wa-select>
|
||||||
|
<div class="page-numbers">
|
||||||
|
<button class="page-btn" disabled><wa-icon name="chevron-left"></wa-icon></button>
|
||||||
|
<button class="page-btn active">1</button>
|
||||||
|
<button class="page-btn">2</button>
|
||||||
|
<button class="page-btn">3</button>
|
||||||
|
<span style="padding: 0 var(--wa-space-xs); color: var(--wa-color-neutral-400);">...</span>
|
||||||
|
<button class="page-btn">9</button>
|
||||||
|
<button class="page-btn"><wa-icon name="chevron-right"></wa-icon></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TokensPanel(tokens []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 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 []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 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() {
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
|
||||||
|
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-tabs wa-tab-panel { padding: var(--wa-space-l) 0; }
|
||||||
|
.dashboard-tabs wa-tab-panel::part(base) { padding: 0; }
|
||||||
|
.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>
|
||||||
|
}
|
||||||
2341
views/dashboard_templ.go
Normal file
2341
views/dashboard_templ.go
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user