chore(init): Setup Makefile for build and test automation
This commit is contained in:
13
Makefile
Normal file
13
Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
all: gen start open
|
||||
|
||||
gen:
|
||||
@templ generate
|
||||
|
||||
start:
|
||||
@go run main.go
|
||||
|
||||
open:
|
||||
@open http://localhost:8080
|
||||
|
||||
.PHONY: gen start
|
||||
1768
_migrate/accounts.html
Normal file
1768
_migrate/accounts.html
Normal file
File diff suppressed because it is too large
Load Diff
882
_migrate/activity.html
Normal file
882
_migrate/activity.html
Normal file
@@ -0,0 +1,882 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: activity.html → views/activity.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- Activity dashboard showing sessions, apps, and security events
|
||||
- Stats cards: Active Sessions, Connected Apps, Pending alerts, Last Active
|
||||
- Pending Actions section with actionable items (review, sign, block)
|
||||
- Active Sessions list with revoke capability
|
||||
- Connected Apps list with permission management
|
||||
- Activity Log with collapsible day groups and category filtering
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ ActivityPage(data ActivityData) {
|
||||
@layouts.DashboardLayout("activity") {
|
||||
@PageHeader("Activity", "Sessions, apps, and recent actions")
|
||||
@ActivityStatsGrid(data.Stats)
|
||||
@PendingActionsCard(data.PendingActions)
|
||||
<div class="content-grid">
|
||||
@ActiveSessionsCard(data.Sessions)
|
||||
@ConnectedAppsCard(data.Apps)
|
||||
@ActivityLogCard(data.ActivityLog)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Refresh button: hx-get="/api/activity" hx-target="#activity-content" hx-indicator="#refresh-icon"
|
||||
- Pending action buttons: hx-post="/api/activity/action" hx-vals='{"action":"approve","id":"123"}'
|
||||
- Dismiss action: hx-delete="/api/activity/pending/123" hx-target="closest article" hx-swap="outerHTML"
|
||||
- Revoke session: hx-delete="/api/sessions/abc" hx-target="closest article" hx-swap="delete"
|
||||
- Disconnect app: hx-delete="/api/connections/xyz" hx-confirm="Disconnect this app?"
|
||||
- Activity filter: hx-get="/api/activity/log?filter=security" hx-target="#activity-log"
|
||||
- Load more: hx-get="/api/activity/log?offset=9" hx-target="#activity-log" hx-swap="beforeend"
|
||||
- wa-details expansion: Client-side (no HTMX needed)
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- ActivityStatsGrid(stats ActivityStats)
|
||||
- PendingActionsCard(actions []PendingAction)
|
||||
- PendingActionItem(action PendingAction)
|
||||
- ActiveSessionsCard(sessions []Session)
|
||||
- SessionItem(session Session, isCurrent bool)
|
||||
- ConnectedAppsCard(apps []ConnectedApp)
|
||||
- AppItem(app ConnectedApp)
|
||||
- ActivityLogCard(logs []ActivityLogGroup, filter string)
|
||||
- ActivityLogGroup(date string, entries []ActivityEntry)
|
||||
- ActivityEntry(entry ActivityEntry)
|
||||
- ActivityFilterSelect(currentFilter string)
|
||||
|
||||
STATE/PROPS:
|
||||
type ActivityData struct {
|
||||
Stats ActivityStats
|
||||
PendingActions []PendingAction
|
||||
Sessions []Session
|
||||
Apps []ConnectedApp
|
||||
ActivityLog []ActivityLogGroup
|
||||
}
|
||||
|
||||
type ActivityStats struct {
|
||||
ActiveSessions int
|
||||
ConnectedApps int
|
||||
PendingCount int
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
type PendingAction struct {
|
||||
ID string
|
||||
Type string // "security", "signature", "suspicious", "transaction"
|
||||
Title string
|
||||
Description string
|
||||
Timestamp time.Time
|
||||
Actions []ActionButton // Review, Dismiss, Sign, Block, etc.
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
Device string
|
||||
Browser string
|
||||
Location string
|
||||
IsCurrent bool
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type ConnectedApp struct {
|
||||
ID string
|
||||
Name string
|
||||
Domain string
|
||||
IconColor string
|
||||
Permissions []string
|
||||
ConnectedAt time.Time
|
||||
}
|
||||
|
||||
type ActivityLogGroup struct {
|
||||
Date string
|
||||
Entries []ActivityEntry
|
||||
}
|
||||
|
||||
type ActivityEntry struct {
|
||||
Type string // "signin", "auth", "send", "receive", "connect", "swap", "passkey"
|
||||
Title string
|
||||
Description string
|
||||
Timestamp time.Time
|
||||
Tag string
|
||||
TagVariant string
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Refresh with loading indicator
|
||||
<wa-button id="refresh-activity"
|
||||
hx-get="/api/activity/refresh"
|
||||
hx-target="#activity-content"
|
||||
hx-indicator="find wa-icon">
|
||||
|
||||
// Pending action dismiss with animation
|
||||
<wa-button hx-delete="/api/activity/pending/{action.ID}"
|
||||
hx-target="closest article"
|
||||
hx-swap="outerHTML swap:0.3s"
|
||||
hx-on::before-request="this.closest('article').style.opacity='0.5'">
|
||||
|
||||
// Session revoke
|
||||
<wa-button hx-delete="/api/sessions/{session.ID}"
|
||||
hx-target="closest article"
|
||||
hx-swap="delete"
|
||||
hx-confirm="Revoke this session?">
|
||||
|
||||
// Activity log filtering
|
||||
<wa-select hx-get="/api/activity/log"
|
||||
hx-target="#activity-log-entries"
|
||||
hx-include="this"
|
||||
name="filter">
|
||||
|
||||
// Load more with append
|
||||
<wa-button hx-get="/api/activity/log?offset=9"
|
||||
hx-target="#activity-log-entries"
|
||||
hx-swap="beforeend">
|
||||
|
||||
// Sign out all sessions
|
||||
<wa-button hx-delete="/api/sessions/all"
|
||||
hx-confirm="Sign out of all other sessions?"
|
||||
hx-target="#sessions-list">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Activity - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--wa-color-neutral-200);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--wa-space-s) var(--wa-space-xs);
|
||||
margin-bottom: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s) var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
text-decoration: none;
|
||||
color: var(--wa-color-neutral-700);
|
||||
font-size: var(--wa-font-size-s);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--wa-space-xl);
|
||||
background: var(--wa-color-surface-alt);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
--min-column-size: 180px;
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
wa-details::part(header) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
wa-details::part(content) {
|
||||
padding-top: var(--wa-space-s);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Sonr Wallet</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="accounts.html">
|
||||
<wa-icon name="wallet"></wa-icon>
|
||||
Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="transactions.html">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
Transactions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="tokens.html">
|
||||
<wa-icon name="coins"></wa-icon>
|
||||
Tokens
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="nfts.html">
|
||||
<wa-icon name="image"></wa-icon>
|
||||
NFTs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="activity.html" class="active">
|
||||
<wa-icon name="chart-line"></wa-icon>
|
||||
Activity
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
|
||||
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="connections.html">
|
||||
<wa-icon name="plug"></wa-icon>
|
||||
Connections
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="device.html">
|
||||
<wa-icon name="mobile"></wa-icon>
|
||||
Devices
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="service.html">
|
||||
<wa-icon name="server"></wa-icon>
|
||||
Services
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="settings.html">
|
||||
<wa-icon name="gear"></wa-icon>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xl">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" id="refresh-activity">
|
||||
<wa-icon slot="start" name="rotate"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
</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="desktop" 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);">Active Sessions</span>
|
||||
<span class="wa-heading-2xl">3</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="plug" 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);">Connected Apps</span>
|
||||
<span class="wa-heading-2xl">5</span>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-flank">
|
||||
<wa-avatar shape="rounded" style="background: var(--wa-color-warning-subtle);">
|
||||
<wa-icon slot="icon" name="bell" style="color: var(--wa-color-warning);"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Pending</span>
|
||||
<span class="wa-cluster wa-gap-xs">
|
||||
<span class="wa-heading-2xl">4</span>
|
||||
<wa-badge variant="warning" attention="pulse" pill>!</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-neutral-100);">
|
||||
<wa-icon slot="icon" name="clock-rotate-left" style="color: var(--wa-color-neutral-600);"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Last Active</span>
|
||||
<wa-relative-time date="2026-01-03T01:49:00" class="wa-heading-s"></wa-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
<wa-card style="margin-bottom: var(--wa-space-xl);">
|
||||
<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">
|
||||
<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="shield-halved" style="color: var(--wa-color-warning);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Security Review Required</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>New device signed in from</span>
|
||||
<strong>San Francisco, CA</strong>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-03T01:40:00" class="wa-caption-s"></wa-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-button variant="brand" size="small">Review</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Dismiss</wa-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<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="signature" style="color: var(--wa-color-primary);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Signature Request</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<a href="#">DeFi Protocol</a>
|
||||
<span>wants to verify wallet ownership</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-03T01:25:00" class="wa-caption-s"></wa-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-button variant="brand" size="small">Sign</wa-button>
|
||||
<wa-button variant="danger" appearance="outlined" size="small">Reject</wa-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<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="triangle-exclamation" style="color: var(--wa-color-danger);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Suspicious Activity</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>Multiple failed logins from IP</span>
|
||||
<code style="font-size: var(--wa-font-size-xs);">185.234.xx.xx</code>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-03T00:50:00" class="wa-caption-s"></wa-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-button variant="danger" size="small">Block IP</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Details</wa-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<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="arrow-down" style="color: var(--wa-color-success);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Incoming Transaction</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>From</span>
|
||||
<a href="#">sonr1k4m...9p3q</a>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-02T23:50:00" class="wa-caption-s"></wa-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="success">+ 250 SNR</wa-tag>
|
||||
</article>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<div class="content-grid">
|
||||
<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">
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-icon class="wa-font-size-l" name="desktop" style="color: var(--wa-color-primary);"></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">MacBook Pro</span>
|
||||
<wa-badge variant="success" pill>Current</wa-badge>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>Chrome 120</span>
|
||||
<span>·</span>
|
||||
<span>San Francisco, CA</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-success);">Active now</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-icon class="wa-font-size-l" name="mobile" style="color: var(--wa-color-success);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">iPhone 15 Pro</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>Safari</span>
|
||||
<span>·</span>
|
||||
<span>San Francisco, CA</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-02T23:51:00" class="wa-caption-xs"></wa-relative-time>
|
||||
</div>
|
||||
<wa-button variant="danger" appearance="plain" size="small">Revoke</wa-button>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-icon class="wa-font-size-l" name="tablet" style="color: var(--wa-color-warning);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">iPad Air</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>Safari</span>
|
||||
<span>·</span>
|
||||
<span>New York, NY</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-relative-time date="2026-01-02T12:00:00" class="wa-caption-xs"></wa-relative-time>
|
||||
</div>
|
||||
<wa-button variant="danger" appearance="plain" size="small">Revoke</wa-button>
|
||||
</article>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-avatar initials="D" style="--size: 36px; background: linear-gradient(135deg, #6366f1, #8b5cf6);"></wa-avatar>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">DeFi Protocol</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">defi.example.com</span>
|
||||
</div>
|
||||
<wa-relative-time date="2025-12-31T12:00:00" class="wa-caption-xs"></wa-relative-time>
|
||||
</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>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-avatar initials="N" style="--size: 36px; background: linear-gradient(135deg, #10b981, #059669);"></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">NFT Marketplace</span>
|
||||
<wa-badge variant="warning" pill>transact</wa-badge>
|
||||
</div>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">nft.marketplace.io</span>
|
||||
</div>
|
||||
<wa-relative-time date="2025-12-27T12:00:00" class="wa-caption-xs"></wa-relative-time>
|
||||
</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>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank wa-gap-m">
|
||||
<wa-avatar initials="S" style="--size: 36px; background: linear-gradient(135deg, #f59e0b, #d97706);"></wa-avatar>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Staking Dashboard</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">stake.sonr.io</span>
|
||||
</div>
|
||||
<wa-relative-time date="2025-12-20T12:00:00" class="wa-caption-xs"></wa-relative-time>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<a href="connections.html" 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>
|
||||
|
||||
<wa-card class="activity-card">
|
||||
<div slot="header" class="wa-flank">
|
||||
<span class="wa-heading-m">Activity Log</span>
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-select size="small" value="all" style="min-width: 130px;">
|
||||
<wa-option value="all">All Activity</wa-option>
|
||||
<wa-option value="security">Security</wa-option>
|
||||
<wa-option value="transactions">Transactions</wa-option>
|
||||
<wa-option value="connections">Connections</wa-option>
|
||||
</wa-select>
|
||||
<wa-button appearance="plain" size="small">
|
||||
<wa-icon slot="start" name="download"></wa-icon>
|
||||
Export
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack">
|
||||
<wa-details open>
|
||||
<span class="wa-heading-m" slot="summary">Today</span>
|
||||
<div class="wa-stack">
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="right-to-bracket" style="color: var(--wa-color-success);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Signed in</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<a href="#">MacBook Pro</a>
|
||||
<span>in San Francisco, CA</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-03T01:51:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="success" pill>success</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="shield-check" style="color: var(--wa-color-primary);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Passkey authenticated</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-600);">WebAuthn verification</span>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-03T01:51:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="neutral" pill>auth</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="arrow-up" style="color: var(--wa-color-danger);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Sent tokens</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>To</span>
|
||||
<a href="#">sonr1k4m...9p3q</a>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-03T11:22:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="danger">- 500 SNR</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="plug" style="color: var(--wa-color-primary);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Connected app</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<a href="#">DeFi Protocol</a>
|
||||
<span>granted access</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-03T10:15:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="brand" pill>connect</wa-tag>
|
||||
</article>
|
||||
</div>
|
||||
</wa-details>
|
||||
|
||||
<wa-details>
|
||||
<span class="wa-heading-m" slot="summary">Yesterday</span>
|
||||
<div class="wa-stack">
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="arrow-down" style="color: var(--wa-color-success);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Received tokens</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>From</span>
|
||||
<a href="#">0x7d3e...9f7a</a>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-02T16:48:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="success">+ 10 AVAX</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="right-to-bracket" style="color: var(--wa-color-success);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Signed in</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<a href="#">iPhone 15 Pro</a>
|
||||
<span>in San Francisco, CA</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-02T14:15:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="success" pill>success</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="signature" style="color: var(--wa-color-neutral-500);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Signed message</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>For</span>
|
||||
<a href="#">NFT Marketplace</a>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-02T09:30:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="neutral" pill>sign</wa-tag>
|
||||
</article>
|
||||
</div>
|
||||
</wa-details>
|
||||
|
||||
<wa-details>
|
||||
<span class="wa-heading-m" slot="summary">January 1, 2026</span>
|
||||
<div class="wa-stack">
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="arrow-right-arrow-left" style="color: var(--wa-color-primary);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Swapped tokens</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>0.5 ETH</span>
|
||||
<wa-icon name="arrow-right" style="font-size: 10px;"></wa-icon>
|
||||
<span>1,172.50 USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-01T09:15:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="brand" pill>swap</wa-tag>
|
||||
</article>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch;">
|
||||
<div class="wa-flank wa-gap-m" style="flex: 1;">
|
||||
<wa-icon name="key" style="color: var(--wa-color-warning);"></wa-icon>
|
||||
<div class="wa-split">
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Added passkey</span>
|
||||
<div class="wa-cluster wa-gap-2xs wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
<span>From</span>
|
||||
<a href="#">iPad Air</a>
|
||||
</div>
|
||||
</div>
|
||||
<wa-format-date date="2026-01-01T08:00:00" hour="numeric" minute="numeric" class="wa-caption-s"></wa-format-date>
|
||||
</div>
|
||||
</div>
|
||||
<wa-tag variant="warning" pill>security</wa-tag>
|
||||
</article>
|
||||
</div>
|
||||
</wa-details>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-flank" style="width: 100%;">
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Showing last 9 activities</span>
|
||||
<wa-button appearance="plain" size="small">
|
||||
Load More
|
||||
<wa-icon slot="end" name="chevron-down"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
document.getElementById('refresh-activity').addEventListener('click', function() {
|
||||
const icon = this.querySelector('wa-icon');
|
||||
icon.style.animation = 'spin 0.5s linear';
|
||||
setTimeout(() => icon.style.animation = '', 500);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-action="dismiss"]').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const article = this.closest('article');
|
||||
article.style.opacity = '0';
|
||||
article.style.transition = 'opacity 0.3s';
|
||||
setTimeout(() => {
|
||||
const divider = article.nextElementSibling;
|
||||
if (divider?.tagName === 'WA-DIVIDER') divider.remove();
|
||||
article.remove();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1047
_migrate/asset.html
Normal file
1047
_migrate/asset.html
Normal file
File diff suppressed because it is too large
Load Diff
719
_migrate/authorize.html
Normal file
719
_migrate/authorize.html
Normal file
@@ -0,0 +1,719 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: authorize.html → views/authorize.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- OAuth/OIDC consent screen for third-party app authorization
|
||||
- Three request types via tabs: Connect, Sign Message, Transaction
|
||||
- Displays requesting app identity with verification badge
|
||||
- Shows wallet selector and permission scopes
|
||||
- Transaction tab includes swap preview, gas estimates, contract data
|
||||
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
|
||||
|
||||
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
|
||||
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
|
||||
- Card max-width: 48ch (~384px) fits all viewport sizes
|
||||
- Tab panels scroll independently if content exceeds viewport
|
||||
- Transaction details use compact layout on small screens
|
||||
- Approve/Deny buttons always visible in footer
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ AuthorizePage(req AuthRequest, wallet WalletInfo) {
|
||||
@layouts.CenteredCard("Authorize - Sonr Motr Wallet") {
|
||||
@AppIdentityHeader(req.App)
|
||||
<wa-divider></wa-divider>
|
||||
@RequestTypeTabs(req)
|
||||
@AuthFooterActions(req.Type)
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Tab switching: hx-on:wa-tab-show="htmx.ajax('GET', '/authorize/tab/' + event.detail.name, '#tab-content')"
|
||||
- Approve action: hx-post="/api/authorize/approve" hx-include="[name='scopes']" hx-target="#auth-result"
|
||||
- Deny action: hx-post="/api/authorize/deny" hx-target="#auth-result"
|
||||
- Wallet selector: hx-get="/authorize/wallets" hx-target="#wallet-dropdown" hx-trigger="click"
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- AppIdentityHeader(app AppInfo) // Logo, name, verified badge, domain
|
||||
- WalletSelector(wallets []Wallet, selected string)
|
||||
- PermissionItem(icon string, iconClass string, title string, desc string)
|
||||
- PermissionList(permissions []Permission)
|
||||
- MessagePreview(message string, signType string)
|
||||
- TransactionPreview(tx Transaction)
|
||||
- SwapPreview(from TokenAmount, to TokenAmount)
|
||||
- GasEstimate(network string, estimatedGas string, maxFee string)
|
||||
- ContractDataDetails(contract string, function string, data string)
|
||||
- AuthFooterActions(requestType string)
|
||||
- AuthResultSuccess(type string, txHash string)
|
||||
- AuthResultDenied()
|
||||
|
||||
STATE/PROPS:
|
||||
type AuthRequest struct {
|
||||
Type string // "connect", "sign", "transaction"
|
||||
App AppInfo
|
||||
Scopes []string
|
||||
Message string // For sign requests
|
||||
Transaction *TxDetails // For transaction requests
|
||||
State string // OAuth state param
|
||||
Nonce string // For OIDC
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
Name string
|
||||
Domain string
|
||||
LogoURL string
|
||||
Verified bool
|
||||
}
|
||||
|
||||
type TxDetails struct {
|
||||
Type string // "swap", "send", "contract"
|
||||
FromToken TokenAmount
|
||||
ToToken TokenAmount
|
||||
Network string
|
||||
EstimatedGas string
|
||||
MaxFee string
|
||||
Contract string
|
||||
Function string
|
||||
RawData string
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Tab group with HTMX partial loading
|
||||
<wa-tab-group hx-on:wa-tab-show="
|
||||
htmx.ajax('GET', '/authorize/panel/' + event.detail.name, {target: '#panel-content'});
|
||||
htmx.ajax('GET', '/authorize/button/' + event.detail.name, {target: '#approve-btn', swap: 'outerHTML'});
|
||||
">
|
||||
|
||||
// Approve button with loading state
|
||||
<wa-button id="approve-btn" variant="brand"
|
||||
hx-post="/api/authorize/approve"
|
||||
hx-include="[name='scopes'],[name='wallet']"
|
||||
hx-target="#auth-card"
|
||||
hx-indicator="this">
|
||||
|
||||
// Wallet selector dropdown via HTMX
|
||||
<div class="wallet-selector"
|
||||
hx-get="/authorize/wallet-options"
|
||||
hx-target="#wallet-dropdown"
|
||||
hx-trigger="click">
|
||||
|
||||
// Out-of-band result update (closes modal/redirects)
|
||||
templ AuthResult(success bool, redirectURL string) {
|
||||
<div id="auth-card" hx-swap-oob="true">
|
||||
if success {
|
||||
@AuthResultSuccess()
|
||||
<script>setTimeout(() => window.location.href = "{ redirectURL }", 2000)</script>
|
||||
} else {
|
||||
@AuthResultDenied()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Allow Access - Sonr</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
padding: var(--wa-space-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-centered wa-card {
|
||||
width: 100%;
|
||||
max-width: 48ch;
|
||||
}
|
||||
|
||||
/* App identity header */
|
||||
.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-logo img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--wa-radius-m);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
/* Wallet selector */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Permission items */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Transaction preview */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Actions footer */
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
gap: var(--wa-space-s);
|
||||
padding-top: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.auth-actions wa-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Tabs for different request types */
|
||||
.request-tabs wa-tab-panel {
|
||||
padding-top: var(--wa-space-m);
|
||||
}
|
||||
|
||||
/* Raw data toggle */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Viewport constraints for popup/webview (360-480px width) */
|
||||
@media (max-width: 400px) {
|
||||
.main-centered {
|
||||
padding: var(--wa-space-m);
|
||||
}
|
||||
.main-centered wa-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
.app-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.app-logo img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<main class="main-centered">
|
||||
<wa-card>
|
||||
<!-- App Identity Header -->
|
||||
<div class="app-identity">
|
||||
<div class="app-logo">
|
||||
<wa-icon name="cube" 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">Uniswap</span>
|
||||
<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.uniswap.org</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<!-- Request Type Tabs -->
|
||||
<wa-tab-group class="request-tabs">
|
||||
<wa-tab panel="connect">
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-icon name="link"></wa-icon>
|
||||
<span>Connect</span>
|
||||
</div>
|
||||
</wa-tab>
|
||||
<wa-tab panel="sign">
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-icon name="pen-nib"></wa-icon>
|
||||
<span>Sign</span>
|
||||
</div>
|
||||
</wa-tab>
|
||||
<wa-tab panel="transaction">
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-icon name="paper-plane"></wa-icon>
|
||||
<span>Transaction</span>
|
||||
</div>
|
||||
</wa-tab>
|
||||
|
||||
<!-- Connect Panel -->
|
||||
<wa-tab-panel name="connect">
|
||||
<div class="wa-stack">
|
||||
<!-- Wallet Selection -->
|
||||
<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">Main Wallet</span>
|
||||
<span class="wallet-address">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-icon-button name="chevron-down" variant="plain"></wa-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions List -->
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<span class="wa-heading-s">This app wants to:</span>
|
||||
|
||||
<div class="permission-item">
|
||||
<div class="permission-icon read">
|
||||
<wa-icon name="eye"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-xs">See your wallet address</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Your public address only</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-item">
|
||||
<div class="permission-icon read">
|
||||
<wa-icon name="coins"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-xs">See your balances</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Token amounts in your wallet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-item">
|
||||
<div class="permission-icon write">
|
||||
<wa-icon name="bell"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-xs">Ask to send transactions</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">You'll approve each one separately</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning Callout -->
|
||||
<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>
|
||||
</wa-tab-panel>
|
||||
|
||||
<!-- Sign Message Panel -->
|
||||
<wa-tab-panel name="sign">
|
||||
<div class="wa-stack">
|
||||
<!-- Signing Wallet -->
|
||||
<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">Main Wallet</span>
|
||||
<span class="wallet-address">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-badge variant="neutral">Signing</wa-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Preview -->
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<span class="wa-heading-s">You're signing this message:</span>
|
||||
<div class="tx-preview">
|
||||
Welcome to Uniswap!
|
||||
|
||||
Click to sign in and accept the Uniswap Terms of Service.
|
||||
|
||||
This request will not trigger a blockchain transaction or cost any gas fees.
|
||||
|
||||
Wallet address:
|
||||
sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m
|
||||
|
||||
Nonce: 8f4a2b1c
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sign Type Info -->
|
||||
<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>
|
||||
|
||||
<!-- Advanced: Raw Data Toggle -->
|
||||
<div class="raw-data-section">
|
||||
<wa-details summary="Show technical details">
|
||||
<div class="raw-data-content">
|
||||
0x57656c636f6d6520746f20556e697377617021
|
||||
|
||||
436c69636b20746f207369676e20696e20616e642061636365707420746865
|
||||
20556e6973776170205465726d73206f6620536572766963652e0a0a5468
|
||||
69732072657175657374200a0a57616c6c65742061646472657373
|
||||
</div>
|
||||
</wa-details>
|
||||
</div>
|
||||
</div>
|
||||
</wa-tab-panel>
|
||||
|
||||
<!-- Transaction Panel -->
|
||||
<wa-tab-panel name="transaction">
|
||||
<div class="wa-stack">
|
||||
<!-- From Wallet -->
|
||||
<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">Main Wallet</span>
|
||||
<span class="wallet-address">sonr1x9f...7k2m</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">1,234.56 SNR</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Details Card -->
|
||||
<wa-card>
|
||||
<div class="wa-stack wa-gap-s">
|
||||
<div class="wa-flank">
|
||||
<span class="wa-heading-m">Swap</span>
|
||||
<wa-badge variant="warning">Waiting</wa-badge>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<!-- Swap Visual -->
|
||||
<div class="wa-stack wa-gap-s">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-avatar initials="E" style="--size: 32px; background: var(--wa-color-neutral-200);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">100.00 ETH</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Ethereum</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-danger);">-$234,567.00</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="U" style="--size: 32px; background: var(--wa-color-primary-subtle);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">~125,000 USDC</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">USD Coin</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-success);">+$125,000.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<!-- Gas & Fee Details -->
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<div class="tx-detail-row">
|
||||
<span class="tx-label">Network</span>
|
||||
<span class="tx-value">Sonr Mainnet</span>
|
||||
</div>
|
||||
<div class="tx-detail-row">
|
||||
<span class="tx-label">Network fee</span>
|
||||
<span class="tx-value">~$0.12</span>
|
||||
</div>
|
||||
<div class="tx-detail-row">
|
||||
<span class="tx-label">Max fee</span>
|
||||
<span class="tx-value">$0.26</span>
|
||||
</div>
|
||||
<div class="tx-detail-row">
|
||||
<span class="tx-label">Price change limit</span>
|
||||
<span class="tx-value">0.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Danger Warning for High Value -->
|
||||
<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>
|
||||
|
||||
<!-- Advanced: Contract Data -->
|
||||
<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">0x7a25...3f8b</span>
|
||||
</div>
|
||||
<div class="tx-detail-row">
|
||||
<span class="tx-label">Function</span>
|
||||
<span class="tx-value">swapExactTokensForTokens()</span>
|
||||
</div>
|
||||
<div class="raw-data-content">
|
||||
0x38ed1739
|
||||
0000000000000000000000000000000000000056bc75e2d63100000
|
||||
000000000000000000000000000000000000001e8480
|
||||
00000000000000000000000000000000000000000000000000000060
|
||||
0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659
|
||||
</div>
|
||||
</div>
|
||||
</wa-details>
|
||||
</div>
|
||||
</div>
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
|
||||
<!-- Footer Actions -->
|
||||
<div slot="footer">
|
||||
<div class="wa-stack wa-gap-s">
|
||||
<div class="auth-actions">
|
||||
<wa-button variant="neutral" appearance="outlined" id="deny-btn">
|
||||
<wa-icon slot="start" name="x"></wa-icon>
|
||||
Cancel
|
||||
</wa-button>
|
||||
<wa-button variant="brand" id="approve-btn">
|
||||
<wa-icon slot="start" name="check"></wa-icon>
|
||||
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>
|
||||
</div>
|
||||
</wa-card>
|
||||
</main>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
// Tab panel state management
|
||||
const tabGroup = document.querySelector('wa-tab-group');
|
||||
const approveBtn = document.getElementById('approve-btn');
|
||||
const denyBtn = document.getElementById('deny-btn');
|
||||
|
||||
// Update button text based on active panel
|
||||
tabGroup.addEventListener('wa-tab-show', (event) => {
|
||||
const panel = event.detail.name;
|
||||
|
||||
switch (panel) {
|
||||
case 'connect':
|
||||
approveBtn.innerHTML = '<wa-icon slot="start" name="link"></wa-icon>Allow';
|
||||
break;
|
||||
case 'sign':
|
||||
approveBtn.innerHTML = '<wa-icon slot="start" name="pen-nib"></wa-icon>Sign';
|
||||
break;
|
||||
case 'transaction':
|
||||
approveBtn.innerHTML = '<wa-icon slot="start" name="paper-plane"></wa-icon>Confirm';
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Simulated approve action
|
||||
approveBtn.addEventListener('click', async () => {
|
||||
const activePanel = tabGroup.querySelector('wa-tab[active]')?.getAttribute('panel') || 'connect';
|
||||
|
||||
approveBtn.loading = true;
|
||||
approveBtn.disabled = true;
|
||||
denyBtn.disabled = true;
|
||||
|
||||
// Simulate WebAuthn authentication
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Show success
|
||||
const card = document.querySelector('wa-card');
|
||||
card.innerHTML = `
|
||||
<div class="wa-stack" 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">${activePanel === 'connect' ? 'Connected' : activePanel === 'sign' ? 'Signed' : 'Sent'}</span>
|
||||
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
|
||||
${activePanel === 'transaction' ? 'Transaction ID: 0x8f4a2b1c...' : 'You can close this window.'}
|
||||
</span>
|
||||
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
|
||||
Close
|
||||
</wa-button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Deny action
|
||||
denyBtn.addEventListener('click', () => {
|
||||
const card = document.querySelector('wa-card');
|
||||
card.innerHTML = `
|
||||
<div class="wa-stack" 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>
|
||||
`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1082
_migrate/charts.js
Normal file
1082
_migrate/charts.js
Normal file
File diff suppressed because it is too large
Load Diff
860
_migrate/connections.html
Normal file
860
_migrate/connections.html
Normal file
@@ -0,0 +1,860 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: connections.html → views/connections.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- Connected apps management with detailed analytics cards
|
||||
- Stats: Connected Apps count, Signatures (30d), Transactions (30d), Last Activity
|
||||
- App cards showing session activity %, signatures, permission usage, scopes
|
||||
- Manage dialog for editing permissions
|
||||
- Disconnect confirmation dialog
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ ConnectionsPage(data ConnectionsData) {
|
||||
@layouts.DashboardLayout("connections") {
|
||||
@PageHeader("Connections", "Manage applications connected to your wallet")
|
||||
@ConnectionsStatsGrid(data.Stats)
|
||||
@ConnectedAppsGrid(data.Apps)
|
||||
}
|
||||
@ManageConnectionDialog()
|
||||
@DisconnectConfirmDialog()
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Disconnect app: hx-delete="/api/connections/{app.ID}" hx-target="closest .app-card" hx-swap="delete"
|
||||
- Manage dialog: hx-get="/connections/{app.ID}/manage" hx-target="#manage-dialog-content"
|
||||
- Update permissions: hx-post="/api/connections/{app.ID}/permissions" hx-include="[name^='scope-']"
|
||||
- Revoke OAuth client: hx-delete="/api/oauth/clients/{clientID}" hx-target="closest .oauth-client-card"
|
||||
- Stats refresh: hx-get="/api/connections/stats" hx-trigger="every 60s" hx-target="#stats-grid"
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- ConnectionsStatsGrid(stats ConnectionStats)
|
||||
- ConnectedAppsGrid(apps []ConnectedApp)
|
||||
- ConnectedAppCard(app ConnectedApp)
|
||||
- AppHeader(app AppInfo, showDetails bool)
|
||||
- SessionActivityRing(percent int, trend string, trendValue string)
|
||||
- SignatureCount(count int, status string)
|
||||
- PermissionUsage(used int, total int)
|
||||
- AppMetadata(connectedAt time.Time, lastUsed time.Time, txCount int)
|
||||
- ScopeTagList(scopes []Scope)
|
||||
- ScopeTag(scope Scope)
|
||||
- ManageConnectionDialog(app ConnectedApp)
|
||||
- PermissionCheckbox(scope Scope, enabled bool)
|
||||
- DisconnectConfirmDialog(appName string)
|
||||
|
||||
STATE/PROPS:
|
||||
type ConnectionsData struct {
|
||||
Stats ConnectionStats
|
||||
Apps []ConnectedApp
|
||||
}
|
||||
|
||||
type ConnectionStats struct {
|
||||
ConnectedApps int
|
||||
Signatures30d int
|
||||
Transactions30d int
|
||||
LastActivity time.Time
|
||||
}
|
||||
|
||||
type ConnectedApp struct {
|
||||
ID string
|
||||
Name string
|
||||
Domain string
|
||||
LogoColor string
|
||||
IconName string
|
||||
IsVerified bool
|
||||
SessionActivity int // Percentage
|
||||
ActivityTrend string // "up", "down"
|
||||
TrendValue string // "+15%", "-5%"
|
||||
SignatureCount int
|
||||
Status string // "active", "idle"
|
||||
PermissionsUsed int
|
||||
PermissionsTotal int
|
||||
ConnectedAt time.Time
|
||||
LastUsed time.Time
|
||||
TxApproved int
|
||||
Scopes []Scope
|
||||
}
|
||||
|
||||
type Scope struct {
|
||||
Name string // "wallet:read", "wallet:sign", etc.
|
||||
Type string // "read", "write", "sign"
|
||||
Description string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// App card with expand/manage
|
||||
<wa-button hx-get="/connections/{app.ID}/manage"
|
||||
hx-target="#manage-dialog-content"
|
||||
hx-swap="innerHTML"
|
||||
onclick="document.getElementById('manage-dialog').open = true">
|
||||
|
||||
// Disconnect with confirmation
|
||||
<wa-button hx-delete="/api/connections/{app.ID}"
|
||||
hx-target="closest .app-card"
|
||||
hx-swap="delete"
|
||||
hx-confirm="Disconnect {app.Name}?">
|
||||
|
||||
// Permission toggle in manage dialog
|
||||
<wa-checkbox hx-post="/api/connections/{app.ID}/scopes"
|
||||
hx-include="this"
|
||||
hx-target="#permission-status"
|
||||
name="scope-wallet:read">
|
||||
|
||||
// Revoke all permissions (disconnect via dialog)
|
||||
<wa-button hx-delete="/api/connections/{app.ID}"
|
||||
hx-target="#manage-dialog"
|
||||
hx-swap="innerHTML"
|
||||
hx-on::after-request="document.getElementById('manage-dialog').open = false">
|
||||
|
||||
// Activity stats auto-refresh
|
||||
<div id="stats-grid"
|
||||
hx-get="/api/connections/stats"
|
||||
hx-trigger="every 60s"
|
||||
hx-swap="innerHTML">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Connections - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--wa-color-neutral-200);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--wa-space-s) var(--wa-space-xs);
|
||||
margin-bottom: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s) var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
text-decoration: none;
|
||||
color: var(--wa-color-neutral-700);
|
||||
font-size: var(--wa-font-size-s);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--wa-space-xl);
|
||||
background: var(--wa-color-surface-alt);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--wa-space-l);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.apps-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--wa-radius-m);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.scope-tag {
|
||||
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);
|
||||
}
|
||||
|
||||
.scope-tag.read {
|
||||
background: var(--wa-color-success-subtle);
|
||||
color: var(--wa-color-success);
|
||||
}
|
||||
|
||||
.scope-tag.write {
|
||||
background: var(--wa-color-warning-subtle);
|
||||
color: var(--wa-color-warning);
|
||||
}
|
||||
|
||||
.scope-tag.sign {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--wa-space-3xl) var(--wa-space-xl);
|
||||
color: var(--wa-color-neutral-500);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
--min-column-size: 220px;
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Sonr Wallet</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="accounts.html">
|
||||
<wa-icon name="wallet"></wa-icon>
|
||||
Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="transactions.html">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
Transactions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="tokens.html">
|
||||
<wa-icon name="coins"></wa-icon>
|
||||
Tokens
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="nfts.html">
|
||||
<wa-icon name="image"></wa-icon>
|
||||
NFTs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="activity.html">
|
||||
<wa-icon name="chart-line"></wa-icon>
|
||||
Activity
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
|
||||
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="connections.html" class="active">
|
||||
<wa-icon name="plug"></wa-icon>
|
||||
Connections
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="device.html">
|
||||
<wa-icon name="mobile"></wa-icon>
|
||||
Devices
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="service.html">
|
||||
<wa-icon name="server"></wa-icon>
|
||||
Services
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="settings.html">
|
||||
<wa-icon name="gear"></wa-icon>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xl">Connections</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Manage applications connected to your wallet</span>
|
||||
</div>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">
|
||||
<wa-icon slot="start" name="shield-check"></wa-icon>
|
||||
Security Settings
|
||||
</wa-button>
|
||||
</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="plug" 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);">Connected Apps</span>
|
||||
<span class="wa-heading-2xl">4</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="signature" 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);">Signatures (30d)</span>
|
||||
<span class="wa-heading-2xl">47</span>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-flank">
|
||||
<wa-avatar shape="rounded" style="background: var(--wa-color-warning-subtle);">
|
||||
<wa-icon slot="icon" name="paper-plane" style="color: var(--wa-color-warning);"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Transactions (30d)</span>
|
||||
<span class="wa-heading-2xl">12</span>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-flank">
|
||||
<wa-avatar shape="rounded" style="background: var(--wa-color-neutral-100);">
|
||||
<wa-icon slot="icon" name="clock" style="color: var(--wa-color-neutral-600);"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Last Activity</span>
|
||||
<span class="wa-heading-2xl">2h ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
<div class="apps-grid">
|
||||
<wa-card class="app-card" style="max-width: 100%;">
|
||||
<div slot="header" class="wa-split">
|
||||
<div class="app-header">
|
||||
<div class="app-logo" style="background: linear-gradient(135deg, #ff007a, #ff5ca0);">
|
||||
<wa-icon name="arrows-rotate" style="color: white; font-size: 24px;"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<div class="app-name-row">
|
||||
<h3 class="wa-heading-l">Uniswap</h3>
|
||||
<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.uniswap.org</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-button appearance="plain" size="small">
|
||||
<wa-icon id="uniswap-btn" name="chevron-right" label="View Details"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-tooltip for="uniswap-btn">View Details</wa-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xl">
|
||||
<div class="wa-split wa-align-items-stretch">
|
||||
<article class="wa-stack wa-align-items-start wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Session Activity</h4>
|
||||
<div class="wa-cluster wa-heading-3xl">
|
||||
<wa-progress-ring value="78" style="--size: 1em; --track-width: 0.125em"></wa-progress-ring>
|
||||
<span>78%</span>
|
||||
</div>
|
||||
<wa-badge appearance="filled outlined" variant="success">
|
||||
<wa-icon name="arrow-up"></wa-icon> up from 65%
|
||||
</wa-badge>
|
||||
</article>
|
||||
<article class="wa-stack wa-gap-xs wa-align-items-end">
|
||||
<h4 class="wa-caption-m">Total Signatures</h4>
|
||||
<span class="wa-heading-3xl">23</span>
|
||||
<wa-badge appearance="filled outlined" variant="success">
|
||||
<wa-icon name="sparkles"></wa-icon> Active
|
||||
</wa-badge>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-stack wa-gap-xl">
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Permission Usage</h4>
|
||||
<div class="wa-split">
|
||||
<span class="wa-heading-3xl">3</span>
|
||||
<span class="wa-caption-l">of 4 scopes</span>
|
||||
</div>
|
||||
<wa-progress-bar value="75" label="Permissions used"></wa-progress-bar>
|
||||
</div>
|
||||
<dl class="wa-stack wa-caption-s">
|
||||
<div class="wa-cluster">
|
||||
<dt>Connected</dt>
|
||||
<dd>Dec 15, 2025</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Last Used</dt>
|
||||
<dd>2 hours ago</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Transactions</dt>
|
||||
<dd>8 approved</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div class="wa-cluster wa-gap-xs" style="flex-wrap: wrap;">
|
||||
<span class="scope-tag read"><wa-icon name="eye" style="font-size: 12px;"></wa-icon> wallet:read</span>
|
||||
<span class="scope-tag sign"><wa-icon name="pen-nib" style="font-size: 12px;"></wa-icon> wallet:sign</span>
|
||||
<span class="scope-tag write"><wa-icon name="paper-plane" style="font-size: 12px;"></wa-icon> wallet:transact</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
||||
<wa-button variant="danger" appearance="plain" size="small">Disconnect</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Manage</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="app-card" style="max-width: 100%;">
|
||||
<div slot="header" class="wa-split">
|
||||
<div class="app-header">
|
||||
<div class="app-logo" style="background: linear-gradient(135deg, #627eea, #4c63d2);">
|
||||
<wa-icon name="sailboat" style="color: white; font-size: 24px;"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<div class="app-name-row">
|
||||
<h3 class="wa-heading-l">OpenSea</h3>
|
||||
<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);">opensea.io</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-button appearance="plain" size="small">
|
||||
<wa-icon id="opensea-btn" name="chevron-right" label="View Details"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-tooltip for="opensea-btn">View Details</wa-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xl">
|
||||
<div class="wa-split wa-align-items-stretch">
|
||||
<article class="wa-stack wa-align-items-start wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Session Activity</h4>
|
||||
<div class="wa-cluster wa-heading-3xl">
|
||||
<wa-progress-ring value="45" style="--size: 1em; --track-width: 0.125em"></wa-progress-ring>
|
||||
<span>45%</span>
|
||||
</div>
|
||||
<wa-badge appearance="filled outlined" variant="danger">
|
||||
<wa-icon name="arrow-down"></wa-icon> down from 62%
|
||||
</wa-badge>
|
||||
</article>
|
||||
<article class="wa-stack wa-gap-xs wa-align-items-end">
|
||||
<h4 class="wa-caption-m">Total Signatures</h4>
|
||||
<span class="wa-heading-3xl">15</span>
|
||||
<wa-badge appearance="filled outlined" variant="neutral">
|
||||
<wa-icon name="clock"></wa-icon> Idle
|
||||
</wa-badge>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-stack wa-gap-xl">
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Permission Usage</h4>
|
||||
<div class="wa-split">
|
||||
<span class="wa-heading-3xl">2</span>
|
||||
<span class="wa-caption-l">of 3 scopes</span>
|
||||
</div>
|
||||
<wa-progress-bar value="66" label="Permissions used"></wa-progress-bar>
|
||||
</div>
|
||||
<dl class="wa-stack wa-caption-s">
|
||||
<div class="wa-cluster">
|
||||
<dt>Connected</dt>
|
||||
<dd>Nov 28, 2025</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Last Used</dt>
|
||||
<dd>3 days ago</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Transactions</dt>
|
||||
<dd>2 approved</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div class="wa-cluster wa-gap-xs" style="flex-wrap: wrap;">
|
||||
<span class="scope-tag read"><wa-icon name="eye" style="font-size: 12px;"></wa-icon> wallet:read</span>
|
||||
<span class="scope-tag sign"><wa-icon name="pen-nib" style="font-size: 12px;"></wa-icon> wallet:sign</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
||||
<wa-button variant="danger" appearance="plain" size="small">Disconnect</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Manage</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="app-card" style="max-width: 100%;">
|
||||
<div slot="header" class="wa-split">
|
||||
<div class="app-header">
|
||||
<div class="app-logo" style="background: linear-gradient(135deg, #1db954, #169c46);">
|
||||
<wa-icon name="leaf" style="color: white; font-size: 24px;"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<div class="app-name-row">
|
||||
<h3 class="wa-heading-l">Aave</h3>
|
||||
<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.aave.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-button appearance="plain" size="small">
|
||||
<wa-icon id="aave-btn" name="chevron-right" label="View Details"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-tooltip for="aave-btn">View Details</wa-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xl">
|
||||
<div class="wa-split wa-align-items-stretch">
|
||||
<article class="wa-stack wa-align-items-start wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Session Activity</h4>
|
||||
<div class="wa-cluster wa-heading-3xl">
|
||||
<wa-progress-ring value="92" style="--size: 1em; --track-width: 0.125em"></wa-progress-ring>
|
||||
<span>92%</span>
|
||||
</div>
|
||||
<wa-badge appearance="filled outlined" variant="success">
|
||||
<wa-icon name="arrow-up"></wa-icon> up from 88%
|
||||
</wa-badge>
|
||||
</article>
|
||||
<article class="wa-stack wa-gap-xs wa-align-items-end">
|
||||
<h4 class="wa-caption-m">Total Signatures</h4>
|
||||
<span class="wa-heading-3xl">5</span>
|
||||
<wa-badge appearance="filled outlined" variant="success">
|
||||
<wa-icon name="sparkles"></wa-icon> Active
|
||||
</wa-badge>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-stack wa-gap-xl">
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Permission Usage</h4>
|
||||
<div class="wa-split">
|
||||
<span class="wa-heading-3xl">4</span>
|
||||
<span class="wa-caption-l">of 4 scopes</span>
|
||||
</div>
|
||||
<wa-progress-bar value="100" label="Permissions used"></wa-progress-bar>
|
||||
</div>
|
||||
<dl class="wa-stack wa-caption-s">
|
||||
<div class="wa-cluster">
|
||||
<dt>Connected</dt>
|
||||
<dd>Jan 2, 2026</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Last Used</dt>
|
||||
<dd>1 hour ago</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Transactions</dt>
|
||||
<dd>2 approved</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div class="wa-cluster wa-gap-xs" style="flex-wrap: wrap;">
|
||||
<span class="scope-tag read"><wa-icon name="eye" style="font-size: 12px;"></wa-icon> wallet:read</span>
|
||||
<span class="scope-tag read"><wa-icon name="user" style="font-size: 12px;"></wa-icon> profile</span>
|
||||
<span class="scope-tag sign"><wa-icon name="pen-nib" style="font-size: 12px;"></wa-icon> wallet:sign</span>
|
||||
<span class="scope-tag write"><wa-icon name="paper-plane" style="font-size: 12px;"></wa-icon> wallet:transact</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
||||
<wa-button variant="danger" appearance="plain" size="small">Disconnect</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Manage</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="app-card" style="max-width: 100%;">
|
||||
<div slot="header" class="wa-split">
|
||||
<div class="app-header">
|
||||
<div class="app-logo" style="background: linear-gradient(135deg, #f7931a, #ff9500);">
|
||||
<wa-icon name="cube" style="color: white; font-size: 24px;"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<div class="app-name-row">
|
||||
<h3 class="wa-heading-l">Example DApp</h3>
|
||||
</div>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">example-dapp.io</span>
|
||||
</div>
|
||||
</div>
|
||||
<wa-button appearance="plain" size="small">
|
||||
<wa-icon id="example-btn" name="chevron-right" label="View Details"></wa-icon>
|
||||
</wa-button>
|
||||
<wa-tooltip for="example-btn">View Details</wa-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xl">
|
||||
<div class="wa-split wa-align-items-stretch">
|
||||
<article class="wa-stack wa-align-items-start wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Session Activity</h4>
|
||||
<div class="wa-cluster wa-heading-3xl">
|
||||
<wa-progress-ring value="12" style="--size: 1em; --track-width: 0.125em"></wa-progress-ring>
|
||||
<span>12%</span>
|
||||
</div>
|
||||
<wa-badge appearance="filled outlined" variant="danger">
|
||||
<wa-icon name="arrow-down"></wa-icon> down from 35%
|
||||
</wa-badge>
|
||||
</article>
|
||||
<article class="wa-stack wa-gap-xs wa-align-items-end">
|
||||
<h4 class="wa-caption-m">Total Signatures</h4>
|
||||
<span class="wa-heading-3xl">4</span>
|
||||
<wa-badge appearance="filled outlined" variant="warning">
|
||||
<wa-icon name="triangle-alert"></wa-icon> Unverified
|
||||
</wa-badge>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<article class="wa-stack wa-gap-xl">
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<h4 class="wa-caption-m">Permission Usage</h4>
|
||||
<div class="wa-split">
|
||||
<span class="wa-heading-3xl">1</span>
|
||||
<span class="wa-caption-l">of 2 scopes</span>
|
||||
</div>
|
||||
<wa-progress-bar value="50" label="Permissions used"></wa-progress-bar>
|
||||
</div>
|
||||
<dl class="wa-stack wa-caption-s">
|
||||
<div class="wa-cluster">
|
||||
<dt>Connected</dt>
|
||||
<dd>Dec 20, 2025</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Last Used</dt>
|
||||
<dd>2 weeks ago</dd>
|
||||
</div>
|
||||
<div class="wa-cluster">
|
||||
<dt>Transactions</dt>
|
||||
<dd>0 approved</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div class="wa-cluster wa-gap-xs" style="flex-wrap: wrap;">
|
||||
<span class="scope-tag read"><wa-icon name="eye" style="font-size: 12px;"></wa-icon> wallet:read</span>
|
||||
<span class="scope-tag sign"><wa-icon name="pen-nib" style="font-size: 12px;"></wa-icon> wallet:sign</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
||||
<wa-button variant="danger" appearance="plain" size="small">Disconnect</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">Manage</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<wa-dialog label="Manage Connection" id="manage-dialog" style="--width: 480px;">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="app-header" style="padding: var(--wa-space-m); background: var(--wa-color-surface-alt); border-radius: var(--wa-radius-m);">
|
||||
<div class="app-logo" style="background: linear-gradient(135deg, #ff007a, #ff5ca0);">
|
||||
<wa-icon name="arrows-rotate" style="color: white; font-size: 24px;"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<div class="app-name-row">
|
||||
<span class="wa-heading-m">Uniswap</span>
|
||||
<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.uniswap.org</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-s">
|
||||
<span class="wa-heading-s">Permissions</span>
|
||||
<wa-checkbox checked>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span>View wallet address</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">wallet:read</span>
|
||||
</div>
|
||||
</wa-checkbox>
|
||||
<wa-checkbox checked>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span>Request signatures</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">wallet:sign</span>
|
||||
</div>
|
||||
</wa-checkbox>
|
||||
<wa-checkbox checked>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span>Request transactions</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">wallet:transact</span>
|
||||
</div>
|
||||
</wa-checkbox>
|
||||
</div>
|
||||
|
||||
<wa-callout variant="neutral">
|
||||
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
||||
<span class="wa-caption-s">Changes will take effect immediately. The app may need to reconnect.</span>
|
||||
</wa-callout>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-flank" style="width: 100%;">
|
||||
<wa-button variant="danger" appearance="outlined">
|
||||
<wa-icon slot="start" name="plug"></wa-icon>
|
||||
Disconnect
|
||||
</wa-button>
|
||||
<div style="flex: 1;"></div>
|
||||
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
||||
<wa-button variant="brand">Save Changes</wa-button>
|
||||
</div>
|
||||
</wa-dialog>
|
||||
|
||||
<wa-dialog label="Disconnect App" id="disconnect-dialog" style="--width: 400px;">
|
||||
<div class="wa-stack wa-gap-l" style="text-align: center;">
|
||||
<wa-icon name="plug" style="font-size: 48px; color: var(--wa-color-danger);"></wa-icon>
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<span class="wa-heading-m">Disconnect Uniswap?</span>
|
||||
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
|
||||
This will revoke all permissions. You'll need to reconnect to use the app with your wallet.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: center; width: 100%;">
|
||||
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
||||
<wa-button variant="danger">Disconnect</wa-button>
|
||||
</div>
|
||||
</wa-dialog>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
const manageDialog = document.getElementById('manage-dialog');
|
||||
const disconnectDialog = document.getElementById('disconnect-dialog');
|
||||
|
||||
document.querySelectorAll('wa-button[variant="neutral"][appearance="outlined"]').forEach(btn => {
|
||||
if (btn.textContent.trim() === 'Manage') {
|
||||
btn.addEventListener('click', () => {
|
||||
manageDialog.open = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('wa-button[variant="danger"][appearance="plain"]').forEach(btn => {
|
||||
if (btn.textContent.trim() === 'Disconnect') {
|
||||
btn.addEventListener('click', () => {
|
||||
disconnectDialog.open = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (this.getAttribute('href') === '#') {
|
||||
e.preventDefault();
|
||||
}
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(l => l.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2144
_migrate/dashboard.html
Normal file
2144
_migrate/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
654
_migrate/demo.html
Normal file
654
_migrate/demo.html
Normal file
@@ -0,0 +1,654 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: demo.html → views/demo.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- Demo page showcasing auth views in mini browser window webview style
|
||||
- Similar to "Sign in with Google" popup windows
|
||||
- Demonstrates welcome, login, register, and authorize flows in iframe
|
||||
- Controls to switch between views and adjust window size
|
||||
- Simulates how dApps would integrate Sonr authentication
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ DemoPage() {
|
||||
@layouts.Base("Auth Demo - Sonr Motr Wallet") {
|
||||
@DemoHeader()
|
||||
@DemoControls()
|
||||
@WebviewFrame()
|
||||
@IntegrationCodeSamples()
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- View switching: hx-get="/demo/frame/{view}" hx-target="#webview-frame"
|
||||
- Size presets: Client-side JavaScript for iframe resizing
|
||||
- Theme toggle: Client-side for dark/light mode preview
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- DemoHeader()
|
||||
- DemoControls()
|
||||
- ViewSelector(views []string, selected string)
|
||||
- SizePresets(sizes []Size, selected string)
|
||||
- WebviewFrame(src string, width int, height int)
|
||||
- IntegrationCodeSamples()
|
||||
- CodeBlock(language string, code string)
|
||||
|
||||
STATE/PROPS:
|
||||
type DemoState struct {
|
||||
CurrentView string // "welcome", "login", "register", "authorize"
|
||||
FrameWidth int
|
||||
FrameHeight int
|
||||
Theme string // "light", "dark", "system"
|
||||
}
|
||||
|
||||
type Size struct {
|
||||
Name string
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Auth Demo - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.demo-layout {
|
||||
min-height: 100vh;
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: var(--wa-color-surface);
|
||||
border-bottom: 1px solid var(--wa-color-neutral-200);
|
||||
padding: var(--wa-space-m) var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.demo-header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
background: var(--wa-color-surface);
|
||||
border-radius: var(--wa-radius-l);
|
||||
padding: var(--wa-space-l);
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.controls-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--wa-space-l);
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.webview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-l);
|
||||
}
|
||||
|
||||
.webview-wrapper {
|
||||
position: relative;
|
||||
background: var(--wa-color-neutral-900);
|
||||
border-radius: var(--wa-radius-l);
|
||||
padding: var(--wa-space-xs);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--wa-color-neutral-700),
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.webview-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-xs) var(--wa-space-s);
|
||||
background: var(--wa-color-neutral-800);
|
||||
border-radius: var(--wa-radius-m) var(--wa-radius-m) 0 0;
|
||||
}
|
||||
|
||||
.titlebar-buttons {
|
||||
display: flex;
|
||||
gap: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.titlebar-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.titlebar-btn.close { background: #ff5f57; }
|
||||
.titlebar-btn.minimize { background: #febc2e; }
|
||||
.titlebar-btn.maximize { background: #28c840; }
|
||||
|
||||
.titlebar-url {
|
||||
flex: 1;
|
||||
background: var(--wa-color-neutral-700);
|
||||
border-radius: var(--wa-radius-s);
|
||||
padding: var(--wa-space-2xs) var(--wa-space-s);
|
||||
font-family: var(--wa-font-mono);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
color: var(--wa-color-neutral-300);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.titlebar-url wa-icon {
|
||||
font-size: 10px;
|
||||
color: var(--wa-color-success);
|
||||
}
|
||||
|
||||
.webview-frame {
|
||||
border: none;
|
||||
border-radius: 0 0 var(--wa-radius-m) var(--wa-radius-m);
|
||||
background: white;
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
}
|
||||
|
||||
.size-indicator {
|
||||
font-family: var(--wa-font-mono);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
color: var(--wa-color-neutral-500);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.integration-section {
|
||||
margin-top: var(--wa-space-2xl);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: var(--wa-color-neutral-900);
|
||||
border-radius: var(--wa-radius-m);
|
||||
padding: var(--wa-space-m);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
font-family: var(--wa-font-mono);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
color: var(--wa-color-neutral-100);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-block .comment { color: var(--wa-color-neutral-500); }
|
||||
.code-block .keyword { color: #ff79c6; }
|
||||
.code-block .string { color: #f1fa8c; }
|
||||
.code-block .function { color: #50fa7b; }
|
||||
.code-block .number { color: #bd93f9; }
|
||||
|
||||
.view-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--wa-space-m);
|
||||
margin-top: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.view-card {
|
||||
background: var(--wa-color-surface);
|
||||
border-radius: var(--wa-radius-m);
|
||||
padding: var(--wa-space-m);
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.view-card:hover {
|
||||
border-color: var(--wa-color-primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.view-card.active {
|
||||
border-color: var(--wa-color-primary);
|
||||
background: var(--wa-color-primary-subtle);
|
||||
}
|
||||
|
||||
.view-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--wa-radius-m);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: var(--wa-space-s);
|
||||
}
|
||||
|
||||
.view-card-icon.welcome { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
|
||||
.view-card-icon.login { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
|
||||
.view-card-icon.register { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
|
||||
.view-card-icon.authorize { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
|
||||
|
||||
.popup-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.popup-window {
|
||||
animation: popup-in 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes popup-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="demo-layout">
|
||||
<header class="demo-header">
|
||||
<div class="demo-header-content">
|
||||
<div class="wa-cluster wa-gap-m">
|
||||
<wa-icon name="wallet" family="duotone" style="font-size: 28px; color: var(--wa-color-primary);"></wa-icon>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-m">Sonr Auth Demo</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Mini Browser Webview Showcase</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-button appearance="plain" size="small" onclick="window.open('https://docs.sonr.io', '_blank')">
|
||||
<wa-icon slot="start" name="book"></wa-icon>
|
||||
Docs
|
||||
</wa-button>
|
||||
<wa-button appearance="plain" size="small" onclick="window.open('https://github.com/sonr-io', '_blank')">
|
||||
<wa-icon slot="start" name="github" family="brands"></wa-icon>
|
||||
GitHub
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="demo-main">
|
||||
<div class="wa-stack wa-gap-m" style="text-align: center; margin-bottom: var(--wa-space-xl);">
|
||||
<h1 class="wa-heading-xl">Authentication Webview Demo</h1>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600); max-width: 600px; margin: 0 auto;">
|
||||
Preview how Sonr authentication flows appear in popup windows and embedded webviews,
|
||||
similar to "Sign in with Google" or "Connect Wallet" experiences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-controls">
|
||||
<div class="controls-grid">
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<label class="wa-label-s">View</label>
|
||||
<wa-select id="view-select" value="welcome">
|
||||
<wa-option value="welcome">Welcome (Onboarding)</wa-option>
|
||||
<wa-option value="login">Login (Sign In)</wa-option>
|
||||
<wa-option value="register">Register (Create Account)</wa-option>
|
||||
<wa-option value="authorize">Authorize (OAuth Consent)</wa-option>
|
||||
</wa-select>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<label class="wa-label-s">Window Size</label>
|
||||
<wa-select id="size-select" value="medium">
|
||||
<wa-option value="small">Small (360 × 540)</wa-option>
|
||||
<wa-option value="medium">Medium (420 × 600)</wa-option>
|
||||
<wa-option value="large">Large (480 × 680)</wa-option>
|
||||
<wa-option value="mobile">Mobile (375 × 667)</wa-option>
|
||||
</wa-select>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<label class="wa-label-s">Actions</label>
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-button variant="brand" id="popup-btn">
|
||||
<wa-icon slot="start" name="arrow-up-right-from-square"></wa-icon>
|
||||
Open as Popup
|
||||
</wa-button>
|
||||
<wa-button variant="neutral" appearance="outlined" id="new-tab-btn">
|
||||
<wa-icon slot="start" name="arrow-up-right"></wa-icon>
|
||||
New Tab
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="webview-container">
|
||||
<div class="webview-wrapper" id="webview-wrapper">
|
||||
<div class="webview-titlebar">
|
||||
<div class="titlebar-buttons">
|
||||
<div class="titlebar-btn close"></div>
|
||||
<div class="titlebar-btn minimize"></div>
|
||||
<div class="titlebar-btn maximize"></div>
|
||||
</div>
|
||||
<div class="titlebar-url" id="titlebar-url">
|
||||
<wa-icon name="lock"></wa-icon>
|
||||
<span id="url-display">auth.sonr.io/welcome</span>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
id="webview-frame"
|
||||
class="webview-frame"
|
||||
src="welcome.html"
|
||||
width="420"
|
||||
height="600"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="size-indicator" id="size-indicator">420 × 600</div>
|
||||
</div>
|
||||
|
||||
<div class="view-grid">
|
||||
<div class="view-card active" data-view="welcome">
|
||||
<div class="view-card-icon welcome">
|
||||
<wa-icon name="hand-wave"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-heading-s">Welcome</span>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
|
||||
Onboarding flow with feature highlights and navigation to login/register
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="view-card" data-view="login">
|
||||
<div class="view-card-icon login">
|
||||
<wa-icon name="right-to-bracket"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-heading-s">Login</span>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
|
||||
WebAuthn sign-in with passkey, security key, and QR code options
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="view-card" data-view="register">
|
||||
<div class="view-card-icon register">
|
||||
<wa-icon name="user-plus"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-heading-s">Register</span>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
|
||||
Account creation wizard with device capability detection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="view-card" data-view="authorize">
|
||||
<div class="view-card-icon authorize">
|
||||
<wa-icon name="shield-check"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-heading-s">Authorize</span>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
|
||||
OAuth consent screen for connect, sign, and transaction requests
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="integration-section">
|
||||
<wa-card>
|
||||
<div slot="header">
|
||||
<span class="wa-heading-m">Integration Examples</span>
|
||||
</div>
|
||||
|
||||
<wa-tab-group>
|
||||
<wa-tab panel="popup">Popup Window</wa-tab>
|
||||
<wa-tab panel="iframe">Embedded Iframe</wa-tab>
|
||||
<wa-tab panel="redirect">Redirect Flow</wa-tab>
|
||||
|
||||
<wa-tab-panel name="popup">
|
||||
<div class="wa-stack wa-gap-m">
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
Open authentication in a centered popup window, similar to "Sign in with Google".
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="comment">// Open Sonr Auth as popup window</span>
|
||||
<span class="keyword">function</span> <span class="function">openSonrAuth</span>() {
|
||||
<span class="keyword">const</span> width = <span class="number">420</span>;
|
||||
<span class="keyword">const</span> height = <span class="number">600</span>;
|
||||
<span class="keyword">const</span> left = (screen.width - width) / <span class="number">2</span>;
|
||||
<span class="keyword">const</span> top = (screen.height - height) / <span class="number">2</span>;
|
||||
|
||||
<span class="keyword">const</span> popup = window.<span class="function">open</span>(
|
||||
<span class="string">'https://auth.sonr.io/authorize?client_id=YOUR_APP&scope=openid+profile'</span>,
|
||||
<span class="string">'SonrAuth'</span>,
|
||||
<span class="string">`width=${width},height=${height},left=${left},top=${top}`</span>
|
||||
);
|
||||
|
||||
<span class="comment">// Listen for auth completion</span>
|
||||
window.<span class="function">addEventListener</span>(<span class="string">'message'</span>, (event) => {
|
||||
<span class="keyword">if</span> (event.origin === <span class="string">'https://auth.sonr.io'</span>) {
|
||||
<span class="keyword">const</span> { token, user } = event.data;
|
||||
<span class="function">handleAuthSuccess</span>(token, user);
|
||||
popup.<span class="function">close</span>();
|
||||
}
|
||||
});
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</wa-tab-panel>
|
||||
|
||||
<wa-tab-panel name="iframe">
|
||||
<div class="wa-stack wa-gap-m">
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
Embed authentication inline within a modal or container.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="comment">// Embed Sonr Auth in iframe</span>
|
||||
<span class="keyword">const</span> container = document.<span class="function">getElementById</span>(<span class="string">'auth-container'</span>);
|
||||
|
||||
<span class="keyword">const</span> iframe = document.<span class="function">createElement</span>(<span class="string">'iframe'</span>);
|
||||
iframe.src = <span class="string">'https://auth.sonr.io/authorize?client_id=YOUR_APP&mode=embedded'</span>;
|
||||
iframe.width = <span class="number">420</span>;
|
||||
iframe.height = <span class="number">600</span>;
|
||||
iframe.style.border = <span class="string">'none'</span>;
|
||||
iframe.style.borderRadius = <span class="string">'12px'</span>;
|
||||
|
||||
container.<span class="function">appendChild</span>(iframe);
|
||||
|
||||
<span class="comment">// Handle postMessage from iframe</span>
|
||||
window.<span class="function">addEventListener</span>(<span class="string">'message'</span>, (event) => {
|
||||
<span class="keyword">if</span> (event.origin === <span class="string">'https://auth.sonr.io'</span>) {
|
||||
<span class="keyword">if</span> (event.data.type === <span class="string">'AUTH_SUCCESS'</span>) {
|
||||
<span class="function">handleAuthSuccess</span>(event.data.payload);
|
||||
} <span class="keyword">else if</span> (event.data.type === <span class="string">'AUTH_CANCEL'</span>) {
|
||||
<span class="function">handleAuthCancel</span>();
|
||||
}
|
||||
}
|
||||
});</pre>
|
||||
</div>
|
||||
</div>
|
||||
</wa-tab-panel>
|
||||
|
||||
<wa-tab-panel name="redirect">
|
||||
<div class="wa-stack wa-gap-m">
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
Standard OAuth 2.0 redirect flow for server-side applications.
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="comment">// Redirect to Sonr Auth (OAuth 2.0 flow)</span>
|
||||
<span class="keyword">function</span> <span class="function">redirectToSonrAuth</span>() {
|
||||
<span class="keyword">const</span> params = <span class="keyword">new</span> <span class="function">URLSearchParams</span>({
|
||||
client_id: <span class="string">'YOUR_CLIENT_ID'</span>,
|
||||
redirect_uri: <span class="string">'https://yourapp.com/callback'</span>,
|
||||
response_type: <span class="string">'code'</span>,
|
||||
scope: <span class="string">'openid profile wallet:read'</span>,
|
||||
state: <span class="function">generateRandomState</span>(),
|
||||
nonce: <span class="function">generateRandomNonce</span>()
|
||||
});
|
||||
|
||||
window.location.href = <span class="string">`https://auth.sonr.io/authorize?${params}`</span>;
|
||||
}
|
||||
|
||||
<span class="comment">// Handle callback on your server</span>
|
||||
<span class="comment">// GET /callback?code=AUTH_CODE&state=STATE</span>
|
||||
<span class="keyword">async function</span> <span class="function">handleCallback</span>(code, state) {
|
||||
<span class="keyword">const</span> tokens = <span class="keyword">await</span> <span class="function">exchangeCodeForTokens</span>(code);
|
||||
<span class="keyword">const</span> user = <span class="keyword">await</span> <span class="function">getUserInfo</span>(tokens.access_token);
|
||||
<span class="keyword">return</span> { tokens, user };
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
</wa-card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="popup-overlay" id="popup-overlay">
|
||||
<div class="popup-window">
|
||||
<div class="webview-wrapper">
|
||||
<div class="webview-titlebar">
|
||||
<div class="titlebar-buttons">
|
||||
<div class="titlebar-btn close" id="popup-close"></div>
|
||||
<div class="titlebar-btn minimize"></div>
|
||||
<div class="titlebar-btn maximize"></div>
|
||||
</div>
|
||||
<div class="titlebar-url">
|
||||
<wa-icon name="lock"></wa-icon>
|
||||
<span id="popup-url-display">auth.sonr.io/welcome</span>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
id="popup-frame"
|
||||
class="webview-frame"
|
||||
src="welcome.html"
|
||||
width="420"
|
||||
height="600"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const viewSelect = document.getElementById('view-select');
|
||||
const sizeSelect = document.getElementById('size-select');
|
||||
const webviewFrame = document.getElementById('webview-frame');
|
||||
const popupFrame = document.getElementById('popup-frame');
|
||||
const urlDisplay = document.getElementById('url-display');
|
||||
const popupUrlDisplay = document.getElementById('popup-url-display');
|
||||
const sizeIndicator = document.getElementById('size-indicator');
|
||||
const viewCards = document.querySelectorAll('.view-card');
|
||||
const popupOverlay = document.getElementById('popup-overlay');
|
||||
const popupClose = document.getElementById('popup-close');
|
||||
const popupBtn = document.getElementById('popup-btn');
|
||||
const newTabBtn = document.getElementById('new-tab-btn');
|
||||
|
||||
const views = {
|
||||
welcome: { url: 'welcome.html', path: 'auth.sonr.io/welcome' },
|
||||
login: { url: 'login.html', path: 'auth.sonr.io/login' },
|
||||
register: { url: 'register.html', path: 'auth.sonr.io/register' },
|
||||
authorize: { url: 'authorize.html', path: 'auth.sonr.io/authorize' }
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
small: { width: 360, height: 540 },
|
||||
medium: { width: 420, height: 600 },
|
||||
large: { width: 480, height: 680 },
|
||||
mobile: { width: 375, height: 667 }
|
||||
};
|
||||
|
||||
function updateView(view) {
|
||||
const viewData = views[view];
|
||||
if (!viewData) return;
|
||||
|
||||
webviewFrame.src = viewData.url;
|
||||
popupFrame.src = viewData.url;
|
||||
urlDisplay.textContent = viewData.path;
|
||||
popupUrlDisplay.textContent = viewData.path;
|
||||
|
||||
viewCards.forEach(card => {
|
||||
card.classList.toggle('active', card.dataset.view === view);
|
||||
});
|
||||
|
||||
viewSelect.value = view;
|
||||
}
|
||||
|
||||
function updateSize(size) {
|
||||
const sizeData = sizes[size];
|
||||
if (!sizeData) return;
|
||||
|
||||
webviewFrame.width = sizeData.width;
|
||||
webviewFrame.height = sizeData.height;
|
||||
popupFrame.width = sizeData.width;
|
||||
popupFrame.height = sizeData.height;
|
||||
sizeIndicator.textContent = `${sizeData.width} × ${sizeData.height}`;
|
||||
sizeSelect.value = size;
|
||||
}
|
||||
|
||||
viewSelect.addEventListener('wa-change', (e) => {
|
||||
updateView(e.target.value);
|
||||
});
|
||||
|
||||
sizeSelect.addEventListener('wa-change', (e) => {
|
||||
updateSize(e.target.value);
|
||||
});
|
||||
|
||||
viewCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
updateView(card.dataset.view);
|
||||
});
|
||||
});
|
||||
|
||||
popupBtn.addEventListener('click', () => {
|
||||
popupFrame.src = webviewFrame.src;
|
||||
popupUrlDisplay.textContent = urlDisplay.textContent;
|
||||
popupOverlay.classList.add('active');
|
||||
});
|
||||
|
||||
popupClose.addEventListener('click', () => {
|
||||
popupOverlay.classList.remove('active');
|
||||
});
|
||||
|
||||
popupOverlay.addEventListener('click', (e) => {
|
||||
if (e.target === popupOverlay) {
|
||||
popupOverlay.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
newTabBtn.addEventListener('click', () => {
|
||||
const currentView = viewSelect.value;
|
||||
window.open(views[currentView].url, '_blank');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && popupOverlay.classList.contains('active')) {
|
||||
popupOverlay.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
updateView('welcome');
|
||||
updateSize('medium');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1310
_migrate/device.html
Normal file
1310
_migrate/device.html
Normal file
File diff suppressed because it is too large
Load Diff
561
_migrate/login.html
Normal file
561
_migrate/login.html
Normal file
@@ -0,0 +1,561 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: login.html → views/login.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- WebAuthn sign-in page with multiple authentication methods
|
||||
- Supports passkey (biometrics), security key, and QR code authentication
|
||||
- Includes conditional UI for passkey autofill in username field
|
||||
- Account recovery flow with recovery key or QR code verification
|
||||
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
|
||||
|
||||
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
|
||||
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
|
||||
- Card max-width: 45ch (~360px) fits all viewport sizes
|
||||
- Auth method cards stack vertically, touch-friendly (min 44px height)
|
||||
- QR code step should center and fit within 360px width
|
||||
- Recovery flow inputs sized for constrained width
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ LoginPage(step string, capabilities DeviceCapabilities) {
|
||||
@layouts.CenteredCard("Sign In - Sonr Motr Wallet") {
|
||||
switch step {
|
||||
case "1":
|
||||
@LoginStep1(capabilities)
|
||||
case "qr":
|
||||
@QRAuthStep()
|
||||
case "recovery":
|
||||
@RecoveryStep()
|
||||
case "success":
|
||||
@SuccessStep("Welcome Back!", "/dashboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Replace onclick="goToStep('recovery')" with hx-get="/login?step=recovery" hx-target="#step-content"
|
||||
- Replace onclick="signIn()" with hx-post="/api/auth/signin" hx-target="#step-content"
|
||||
- Auth method selection: hx-post="/login/select-method" hx-vals='{"method":"passkey"}'
|
||||
- QR polling: hx-get="/api/auth/qr-status" hx-trigger="every 2s" hx-target="#qr-status"
|
||||
- Conditional UI: autocomplete="username webauthn" triggers browser passkey autofill
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- AuthMethodCard(method string, icon string, title string, desc string, disabled bool)
|
||||
- QRCodeSection(value string, label string, fallbackText string)
|
||||
- RecoveryForm(method string) // "recovery-key" or "qr-code"
|
||||
- SuccessStep(title string, redirectUrl string)
|
||||
|
||||
STATE/PROPS:
|
||||
type DeviceCapabilities struct {
|
||||
PlatformAuth bool // Face ID, Touch ID, Windows Hello
|
||||
CrossPlatform bool // Security keys (YubiKey)
|
||||
ConditionalUI bool // Passkey autofill support
|
||||
}
|
||||
|
||||
type LoginState struct {
|
||||
Step string
|
||||
SelectedMethod string // "passkey", "security-key", "qr-code"
|
||||
Username string
|
||||
Error string
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Auth method card with HTMX selection
|
||||
<div class="auth-method-card"
|
||||
hx-post="/login/select-method"
|
||||
hx-vals='{"method":"passkey"}'
|
||||
hx-target="#btn-signin"
|
||||
hx-swap="outerHTML">
|
||||
|
||||
// Sign in button enabled via HTMX swap
|
||||
<wa-button id="btn-signin" variant="brand"
|
||||
hx-post="/api/auth/signin"
|
||||
hx-include="[name='username'],[name='method']"
|
||||
hx-target="#step-content">
|
||||
|
||||
// QR code polling for cross-device auth
|
||||
<div id="qr-status"
|
||||
hx-get="/api/auth/qr-status?session=xyz789"
|
||||
hx-trigger="every 2s"
|
||||
hx-target="this">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign In - Sonr</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
padding: var(--wa-space-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-centered wa-card {
|
||||
width: 100%;
|
||||
max-width: 45ch;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.auth-method-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
padding: var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
background: var(--wa-color-surface-alt);
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.auth-method-card:hover {
|
||||
background: var(--wa-color-surface-alt-hover);
|
||||
}
|
||||
|
||||
.auth-method-card.selected {
|
||||
border-color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.auth-method-card.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-method-card wa-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.auth-method-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.auth-method-info h4 {
|
||||
margin: 0 0 var(--wa-space-2xs) 0;
|
||||
}
|
||||
|
||||
.auth-method-info p {
|
||||
margin: 0;
|
||||
color: var(--wa-color-neutral-600);
|
||||
}
|
||||
|
||||
/* Viewport constraints for popup/webview (360-480px width) */
|
||||
@media (max-width: 400px) {
|
||||
.main-centered {
|
||||
padding: var(--wa-space-m);
|
||||
}
|
||||
.main-centered wa-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
.auth-method-card {
|
||||
padding: var(--wa-space-s);
|
||||
gap: var(--wa-space-s);
|
||||
}
|
||||
.auth-method-card wa-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 600px) {
|
||||
.auth-method-card {
|
||||
padding: var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<main class="main-centered">
|
||||
<wa-card>
|
||||
<!-- Step 1: Sign In Options -->
|
||||
<div class="step active" data-step="1">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<h2 class="wa-heading-l">Welcome Back</h2>
|
||||
<p class="wa-caption-s">How would you like to sign in?</p>
|
||||
</div>
|
||||
|
||||
<wa-input
|
||||
name="username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Enter your username"
|
||||
id="login-username"
|
||||
autocomplete="username webauthn"
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
|
||||
</wa-input>
|
||||
|
||||
<div class="wa-stack wa-gap-s" id="auth-methods">
|
||||
<div class="auth-method-card" data-method="passkey" id="method-passkey">
|
||||
<wa-icon family="duotone" name="fingerprint"></wa-icon>
|
||||
<div class="auth-method-info">
|
||||
<h4 class="wa-label-m">Face or Fingerprint</h4>
|
||||
<p class="wa-caption-s">Use your face, fingerprint, or device PIN</p>
|
||||
</div>
|
||||
<wa-spinner style="--size: 1.25rem; display: none;" id="passkey-spinner"></wa-spinner>
|
||||
</div>
|
||||
|
||||
<div class="auth-method-card" data-method="security-key" id="method-security-key">
|
||||
<wa-icon family="duotone" name="key"></wa-icon>
|
||||
<div class="auth-method-info">
|
||||
<h4 class="wa-label-m">Hardware Key</h4>
|
||||
<p class="wa-caption-s">Use a physical security key</p>
|
||||
</div>
|
||||
<wa-spinner style="--size: 1.25rem; display: none;" id="security-key-spinner"></wa-spinner>
|
||||
</div>
|
||||
|
||||
<div class="auth-method-card" data-method="qr-code" id="method-qr">
|
||||
<wa-icon family="duotone" name="qrcode"></wa-icon>
|
||||
<div class="auth-method-info">
|
||||
<h4 class="wa-label-m">Use Another Device</h4>
|
||||
<p class="wa-caption-s">Scan a QR code with your phone</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-between wa-gap-s">
|
||||
<wa-button appearance="plain" size="small" onclick="goToStep('recovery')">
|
||||
Can't sign in?
|
||||
</wa-button>
|
||||
<wa-button variant="brand" id="btn-signin" disabled>
|
||||
Sign In
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: QR Code Authentication -->
|
||||
<div class="step" data-step="qr">
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<h2 class="wa-heading-l">Use Your Phone</h2>
|
||||
<p class="wa-caption-s">Scan this code with your phone camera</p>
|
||||
|
||||
<wa-qr-code
|
||||
value="https://sonr.id/auth?session=xyz789abc123"
|
||||
label="Scan with your phone camera"
|
||||
></wa-qr-code>
|
||||
|
||||
<p class="wa-caption-s">Or copy this link to your phone</p>
|
||||
<wa-input
|
||||
value="sonr.id/a/xyz789abc123"
|
||||
disabled
|
||||
style="width: 100%;"
|
||||
>
|
||||
<wa-copy-button slot="end" value="sonr.id/a/xyz789abc123"></wa-copy-button>
|
||||
</wa-input>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<div class="wa-stack wa-gap-s wa-align-items-center" style="width: 100%;">
|
||||
<wa-spinner id="qr-waiting-spinner"></wa-spinner>
|
||||
<p class="wa-caption-s">Waiting for your phone...</p>
|
||||
</div>
|
||||
|
||||
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)" style="width: 100%;">
|
||||
Back
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Recovery -->
|
||||
<div class="step" data-step="recovery">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<h2 class="wa-heading-l">Recover Your Account</h2>
|
||||
|
||||
<wa-radio-group
|
||||
label="How would you like to recover?"
|
||||
orientation="horizontal"
|
||||
name="recovery-method"
|
||||
value="recovery-key"
|
||||
id="recovery-method-group"
|
||||
>
|
||||
<wa-radio appearance="button" value="recovery-key">Backup Code</wa-radio>
|
||||
<wa-radio appearance="button" value="qr-code">Phone</wa-radio>
|
||||
</wa-radio-group>
|
||||
|
||||
<div id="recovery-key-form">
|
||||
<div class="wa-stack wa-gap-m">
|
||||
<p class="wa-caption-s">
|
||||
Enter the backup code you saved when you created your account.
|
||||
</p>
|
||||
<wa-input
|
||||
name="recovery-key"
|
||||
type="text"
|
||||
label="Backup Code"
|
||||
placeholder="rk_sec_..."
|
||||
id="recovery-key-input"
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="key"></wa-icon>
|
||||
</wa-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recovery-qr-form" style="display: none;">
|
||||
<div class="wa-stack wa-gap-m wa-align-items-center">
|
||||
<p class="wa-caption-s">
|
||||
Scan this code with your phone, then enter the code shown.
|
||||
</p>
|
||||
<wa-qr-code
|
||||
value="https://sonr.id/recover?token=rec123xyz"
|
||||
label="Scan with your phone"
|
||||
></wa-qr-code>
|
||||
<wa-input
|
||||
type="text"
|
||||
placeholder="000000"
|
||||
maxlength="6"
|
||||
style="text-align: center; font-size: 2rem; letter-spacing: 0.5em; max-width: 200px;"
|
||||
id="recovery-code"
|
||||
></wa-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button variant="brand" id="btn-recover">
|
||||
Recover
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Success -->
|
||||
<div class="step" data-step="success">
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<wa-icon name="circle-check" family="duotone" style="font-size: 4rem; color: var(--wa-color-success);"></wa-icon>
|
||||
<h2 class="wa-heading-l">You're In</h2>
|
||||
<p class="wa-caption-s">
|
||||
Signed in successfully.
|
||||
</p>
|
||||
|
||||
<wa-button variant="brand" style="width: 100%;" onclick="window.location.href='dashboard.html'">
|
||||
Go to Dashboard
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer slot="footer" class="wa-cluster wa-justify-content-center wa-gap-m">
|
||||
<span class="wa-caption-s">Don't have an account?</span>
|
||||
<wa-button appearance="plain" size="small" onclick="window.location.href='register.html'">
|
||||
Create Account
|
||||
</wa-button>
|
||||
</footer>
|
||||
</wa-card>
|
||||
</main>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
let selectedMethod = null;
|
||||
let capabilities = {
|
||||
platform: false,
|
||||
crossPlatform: false,
|
||||
conditional: false
|
||||
};
|
||||
|
||||
async function checkCapabilities() {
|
||||
if (!window.PublicKeyCredential) {
|
||||
document.getElementById('method-passkey').classList.add('disabled');
|
||||
document.getElementById('method-security-key').classList.add('disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
capabilities.platform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
if (!capabilities.platform) {
|
||||
document.getElementById('method-passkey').classList.add('disabled');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('method-passkey').classList.add('disabled');
|
||||
}
|
||||
|
||||
capabilities.crossPlatform = true;
|
||||
|
||||
try {
|
||||
if (PublicKeyCredential.isConditionalMediationAvailable) {
|
||||
capabilities.conditional = await PublicKeyCredential.isConditionalMediationAvailable();
|
||||
if (capabilities.conditional) {
|
||||
startConditionalUI();
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function startConditionalUI() {
|
||||
try {
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rpId: window.location.hostname,
|
||||
userVerification: "preferred",
|
||||
timeout: 300000
|
||||
},
|
||||
mediation: "conditional"
|
||||
});
|
||||
|
||||
if (credential) {
|
||||
console.log("Conditional UI authentication successful:", credential);
|
||||
goToStep('success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Conditional UI not used or failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function selectMethod(method) {
|
||||
if (method === 'passkey' && !capabilities.platform) return;
|
||||
if (method === 'security-key' && !capabilities.crossPlatform) return;
|
||||
|
||||
document.querySelectorAll('.auth-method-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
const card = document.querySelector(`[data-method="${method}"]`);
|
||||
if (card && !card.classList.contains('disabled')) {
|
||||
card.classList.add('selected');
|
||||
selectedMethod = method;
|
||||
document.getElementById('btn-signin').removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
if (!selectedMethod) return;
|
||||
|
||||
if (selectedMethod === 'qr-code') {
|
||||
goToStep('qr');
|
||||
simulateQRPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = document.getElementById(
|
||||
selectedMethod === 'passkey' ? 'passkey-spinner' : 'security-key-spinner'
|
||||
);
|
||||
spinner.style.display = 'block';
|
||||
|
||||
try {
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const getOptions = {
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rpId: window.location.hostname,
|
||||
userVerification: selectedMethod === 'passkey' ? "required" : "preferred",
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedMethod === 'security-key') {
|
||||
getOptions.publicKey.allowCredentials = [];
|
||||
}
|
||||
|
||||
const credential = await navigator.credentials.get(getOptions);
|
||||
console.log("Authentication successful:", credential);
|
||||
goToStep('success');
|
||||
} catch (error) {
|
||||
console.error("Authentication failed:", error);
|
||||
alert("Authentication failed. Please try again.");
|
||||
} finally {
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function simulateQRPolling() {
|
||||
setTimeout(() => {
|
||||
const step = document.querySelector('.step[data-step="qr"]');
|
||||
if (step.classList.contains('active')) {
|
||||
goToStep('success');
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function goToStep(step) {
|
||||
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
|
||||
const targetStep = document.querySelector(`.step[data-step="${step}"]`);
|
||||
if (targetStep) {
|
||||
targetStep.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkCapabilities();
|
||||
|
||||
document.querySelectorAll('.auth-method-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
selectMethod(card.dataset.method);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-signin').addEventListener('click', signIn);
|
||||
|
||||
const recoveryMethodGroup = document.getElementById('recovery-method-group');
|
||||
recoveryMethodGroup.addEventListener('wa-change', (e) => {
|
||||
const keyForm = document.getElementById('recovery-key-form');
|
||||
const qrForm = document.getElementById('recovery-qr-form');
|
||||
|
||||
if (e.target.value === 'recovery-key') {
|
||||
keyForm.style.display = 'block';
|
||||
qrForm.style.display = 'none';
|
||||
} else {
|
||||
keyForm.style.display = 'none';
|
||||
qrForm.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-recover').addEventListener('click', () => {
|
||||
const method = recoveryMethodGroup.value;
|
||||
if (method === 'recovery-key') {
|
||||
const key = document.getElementById('recovery-key-input').value;
|
||||
if (key.startsWith('rk_')) {
|
||||
console.log("Recovery key validated:", key);
|
||||
goToStep('success');
|
||||
} else {
|
||||
alert("Please enter a valid recovery key");
|
||||
}
|
||||
} else {
|
||||
const code = document.getElementById('recovery-code').value;
|
||||
if (code.length === 6) {
|
||||
console.log("Recovery code verified:", code);
|
||||
goToStep('success');
|
||||
} else {
|
||||
alert("Please enter a valid 6-digit code");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
850
_migrate/nfts.html
Normal file
850
_migrate/nfts.html
Normal file
@@ -0,0 +1,850 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: nfts.html → views/nfts.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- NFT gallery view with grid layout and collection grouping
|
||||
- Stats: Total NFTs, Estimated Value, Floor Value, Best Performer
|
||||
- Collection cards showing grouped NFT holdings
|
||||
- Filter tabs (All, Listed, Unlisted) with collection/sort dropdowns
|
||||
- NFT cards with hover actions (Send, Hide) and badges (Listed, Rare)
|
||||
- Grid/List view toggle
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ NFTsPage(data NFTsData) {
|
||||
@layouts.DashboardLayout("nfts") {
|
||||
@PageHeader("NFTs", "Your digital collectibles portfolio")
|
||||
@NFTsStatsGrid(data.Stats)
|
||||
@CollectionsCard(data.Collections)
|
||||
@NFTsGalleryCard(data.NFTs, data.Filter)
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Filter tabs: hx-get="/nfts?filter=listed" hx-target="#nft-grid" hx-push-url="true"
|
||||
- Collection filter: hx-get="/nfts?collection=bayc" hx-target="#nft-grid"
|
||||
- Sort dropdown: hx-get="/nfts?sort=value-high" hx-target="#nft-grid"
|
||||
- View toggle: hx-get="/nfts?view=list" hx-target="#nft-grid" (grid vs list)
|
||||
- NFT card click: hx-get="/nfts/{id}/detail" hx-target="#dialog-container" (detail modal)
|
||||
- Send NFT: hx-get="/drawers/send-nft?id={nft.ID}" hx-target="#drawer-container"
|
||||
- Hide NFT: hx-post="/api/nfts/{id}/hide" hx-target="closest .nft-card" hx-swap="outerHTML"
|
||||
- Import NFT: hx-get="/dialogs/import-nft" hx-target="#dialog-container"
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- NFTsStatsGrid(stats NFTStats)
|
||||
- BestPerformerStat(nft NFTItem)
|
||||
- CollectionsCard(collections []Collection)
|
||||
- CollectionCard(collection Collection)
|
||||
- NFTsGalleryCard(nfts []NFTItem, filter NFTFilter)
|
||||
- NFTFilterBar(filter NFTFilter)
|
||||
- ViewToggle(currentView string)
|
||||
- NFTGrid(nfts []NFTItem)
|
||||
- NFTCard(nft NFTItem)
|
||||
- NFTBadge(type string) // "listed", "rare", etc.
|
||||
- NFTActions(nftID string)
|
||||
- NFTDetailDialog(nft NFTItem)
|
||||
|
||||
STATE/PROPS:
|
||||
type NFTsData struct {
|
||||
Stats NFTStats
|
||||
Collections []Collection
|
||||
NFTs []NFTItem
|
||||
Filter NFTFilter
|
||||
}
|
||||
|
||||
type NFTStats struct {
|
||||
TotalNFTs int
|
||||
EstimatedUSD float64
|
||||
FloorETH float64
|
||||
FloorUSD float64
|
||||
BestPerformer *NFTItem
|
||||
BestChange float64
|
||||
}
|
||||
|
||||
type Collection struct {
|
||||
ID string
|
||||
Name string
|
||||
ImageURL string
|
||||
ItemCount int
|
||||
FloorETH float64
|
||||
TotalUSD float64
|
||||
IsVerified bool
|
||||
}
|
||||
|
||||
type NFTItem struct {
|
||||
ID string
|
||||
Name string
|
||||
ImageURL string
|
||||
CollectionName string
|
||||
CollectionID string
|
||||
IsVerified bool
|
||||
FloorETH float64
|
||||
ValueETH float64
|
||||
IsListed bool
|
||||
Rarity string // "rare", "common", etc.
|
||||
}
|
||||
|
||||
type NFTFilter struct {
|
||||
Tab string // "all", "listed", "unlisted"
|
||||
Collection string
|
||||
Sort string // "recent", "value-high", "value-low", "name"
|
||||
View string // "grid", "list"
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Filter tabs with URL state
|
||||
<wa-tab-group hx-on:wa-tab-show="
|
||||
htmx.ajax('GET', '/nfts?filter=' + event.detail.name, {target: '#nft-grid', pushUrl: true})
|
||||
">
|
||||
|
||||
// Collection filter dropdown
|
||||
<wa-select hx-get="/nfts"
|
||||
hx-target="#nft-grid"
|
||||
hx-include="[name='filter'],[name='sort']"
|
||||
name="collection">
|
||||
|
||||
// NFT card with hover actions
|
||||
<div class="nft-card"
|
||||
hx-get="/nfts/{nft.ID}/detail"
|
||||
hx-target="#dialog-container"
|
||||
hx-trigger="click">
|
||||
<div class="nft-actions" hx-on:click="event.stopPropagation()">
|
||||
<wa-icon-button hx-get="/drawers/send-nft?id={nft.ID}" hx-target="#drawer-container">
|
||||
<wa-icon-button hx-post="/api/nfts/{nft.ID}/hide" hx-target="closest .nft-card" hx-swap="delete">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// View toggle (grid/list)
|
||||
<wa-icon-button hx-get="/nfts?view=grid"
|
||||
hx-target="#nft-grid"
|
||||
hx-include="[name='filter'],[name='collection'],[name='sort']">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NFTs - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--wa-color-neutral-200);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--wa-space-s) var(--wa-space-xs);
|
||||
margin-bottom: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s) var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
text-decoration: none;
|
||||
color: var(--wa-color-neutral-700);
|
||||
font-size: var(--wa-font-size-s);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--wa-space-xl);
|
||||
background: var(--wa-color-surface-alt);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
--min-column-size: 180px;
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
margin-bottom: var(--wa-space-l);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.nft-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 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-image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--wa-color-neutral-200), var(--wa-color-neutral-100));
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.collection-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface);
|
||||
border-radius: var(--wa-radius-m);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.collection-card:hover {
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.collection-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--wa-radius-m);
|
||||
overflow: hidden;
|
||||
background: var(--wa-color-neutral-100);
|
||||
}
|
||||
|
||||
.collection-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--wa-space-3xl);
|
||||
color: var(--wa-color-neutral-500);
|
||||
}
|
||||
|
||||
.hidden-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Sonr Wallet</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="accounts.html">
|
||||
<wa-icon name="wallet"></wa-icon>
|
||||
Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="transactions.html">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
Transactions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="tokens.html">
|
||||
<wa-icon name="coins"></wa-icon>
|
||||
Tokens
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="nfts.html" class="active">
|
||||
<wa-icon name="image"></wa-icon>
|
||||
NFTs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="activity.html">
|
||||
<wa-icon name="chart-line"></wa-icon>
|
||||
Activity
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
|
||||
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="connections.html">
|
||||
<wa-icon name="plug"></wa-icon>
|
||||
Connections
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="device.html">
|
||||
<wa-icon name="mobile"></wa-icon>
|
||||
Devices
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="service.html">
|
||||
<wa-icon name="server"></wa-icon>
|
||||
Services
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="settings.html">
|
||||
<wa-icon name="gear"></wa-icon>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xl">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">
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total NFTs</span>
|
||||
<span class="wa-heading-2xl">12</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">across 5 collections</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Estimated Value</span>
|
||||
<span class="wa-heading-2xl">
|
||||
<wa-format-number type="currency" currency="USD" value="8420.50" lang="en-US"></wa-format-number>
|
||||
</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-success);">
|
||||
<wa-icon name="arrow-trend-up" style="font-size: 12px;"></wa-icon>
|
||||
+12.4% this week
|
||||
</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Floor Value</span>
|
||||
<span class="wa-heading-2xl">4.2 ETH</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">$9,851.40</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Best Performer</span>
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-avatar initials="B" style="--size: 24px; background: #ff6b6b;"></wa-avatar>
|
||||
<span class="wa-heading-s">Bored Ape #4521</span>
|
||||
</div>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-success);">+45% this month</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
<wa-card style="margin-bottom: var(--wa-space-xl);">
|
||||
<div slot="header" class="wa-flank">
|
||||
<span class="wa-heading-m">Collections</span>
|
||||
<a href="#" class="wa-caption-s" style="color: var(--wa-color-primary);">View All</a>
|
||||
</div>
|
||||
<div class="wa-grid" style="--min-column-size: 280px;">
|
||||
<div class="collection-card">
|
||||
<div class="collection-avatar">
|
||||
<img src="https://images.unsplash.com/photo-1620641788421-7a1c342ea42e?w=100&h=100&fit=crop" alt="Bored Apes">
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<span class="wa-heading-s">Bored Ape Yacht Club</span>
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 14px;"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">3 items • Floor: 28.5 ETH</span>
|
||||
</div>
|
||||
<span class="wa-heading-s">$198,450</span>
|
||||
</div>
|
||||
|
||||
<div class="collection-card">
|
||||
<div class="collection-avatar">
|
||||
<img src="https://images.unsplash.com/photo-1634973357973-f2ed2657db3c?w=100&h=100&fit=crop" alt="Azuki">
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<span class="wa-heading-s">Azuki</span>
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 14px;"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">2 items • Floor: 8.2 ETH</span>
|
||||
</div>
|
||||
<span class="wa-heading-s">$38,420</span>
|
||||
</div>
|
||||
|
||||
<div class="collection-card">
|
||||
<div class="collection-avatar">
|
||||
<img src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&h=100&fit=crop" alt="Sonr Genesis">
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<span class="wa-heading-s">Sonr Genesis</span>
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 14px;"></wa-icon>
|
||||
</div>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">5 items • Floor: 0.8 ETH</span>
|
||||
</div>
|
||||
<span class="wa-heading-s">$9,380</span>
|
||||
</div>
|
||||
|
||||
<div class="collection-card">
|
||||
<div class="collection-avatar" style="display: flex; align-items: center; justify-content: center;">
|
||||
<wa-icon name="palette" style="color: var(--wa-color-neutral-400);"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
||||
<span class="wa-heading-s">Art Blocks Curated</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">2 items • Floor: 1.2 ETH</span>
|
||||
</div>
|
||||
<span class="wa-heading-s">$5,620</span>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card>
|
||||
<div class="filter-bar">
|
||||
<wa-tab-group style="flex: 1;">
|
||||
<wa-tab panel="all">All NFTs</wa-tab>
|
||||
<wa-tab panel="listed">Listed</wa-tab>
|
||||
<wa-tab panel="unlisted">Unlisted</wa-tab>
|
||||
</wa-tab-group>
|
||||
|
||||
<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="azuki">Azuki</wa-option>
|
||||
<wa-option value="sonr">Sonr Genesis</wa-option>
|
||||
<wa-option value="artblocks">Art Blocks Curated</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-option value="name">Name A-Z</wa-option>
|
||||
</wa-select>
|
||||
|
||||
<div class="view-toggle">
|
||||
<wa-tooltip content="Grid View">
|
||||
<wa-icon-button name="grid-2" label="Grid view" variant="solid"></wa-icon-button>
|
||||
</wa-tooltip>
|
||||
<wa-tooltip content="List View">
|
||||
<wa-icon-button name="list" label="List view"></wa-icon-button>
|
||||
</wa-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-grid">
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1620641788421-7a1c342ea42e?w=400&h=400&fit=crop" alt="Bored Ape #4521">
|
||||
<div class="nft-badge">
|
||||
<wa-badge variant="success" pill>Listed</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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Bored Ape Yacht Club</span>
|
||||
</div>
|
||||
<div class="nft-name">Bored Ape #4521</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 28.5 ETH</span>
|
||||
<span class="nft-value">32.0 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1634973357973-f2ed2657db3c?w=400&h=400&fit=crop" alt="Azuki #8234">
|
||||
<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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Azuki</span>
|
||||
</div>
|
||||
<div class="nft-name">Azuki #8234</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 8.2 ETH</span>
|
||||
<span class="nft-value">9.5 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=400&h=400&fit=crop" alt="Sonr Genesis #001">
|
||||
<div class="nft-badge">
|
||||
<wa-badge variant="brand" pill>Rare</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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Sonr Genesis</span>
|
||||
</div>
|
||||
<div class="nft-name">Sonr Genesis #001</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 0.8 ETH</span>
|
||||
<span class="nft-value">2.5 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1642388738915-eb2c06012af6?w=400&h=400&fit=crop" alt="Sonr Genesis #042">
|
||||
<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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Sonr Genesis</span>
|
||||
</div>
|
||||
<div class="nft-name">Sonr Genesis #042</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 0.8 ETH</span>
|
||||
<span class="nft-value">0.95 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1633101585272-9511440abd03?w=400&h=400&fit=crop" alt="Art Blocks #7821">
|
||||
<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">
|
||||
<span class="nft-collection-name">Art Blocks Curated</span>
|
||||
</div>
|
||||
<div class="nft-name">Chromie Squiggle #7821</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 1.2 ETH</span>
|
||||
<span class="nft-value">1.8 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1639762681485-074b7f938ba0?w=400&h=400&fit=crop" alt="Bored Ape #2187">
|
||||
<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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Bored Ape Yacht Club</span>
|
||||
</div>
|
||||
<div class="nft-name">Bored Ape #2187</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 28.5 ETH</span>
|
||||
<span class="nft-value">30.2 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1644361567152-8a09a9a3fbfb?w=400&h=400&fit=crop" alt="Azuki #5102">
|
||||
<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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Azuki</span>
|
||||
</div>
|
||||
<div class="nft-name">Azuki #5102</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 8.2 ETH</span>
|
||||
<span class="nft-value">8.5 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nft-card">
|
||||
<div class="nft-image">
|
||||
<img src="https://images.unsplash.com/photo-1635322966219-b75ed372eb01?w=400&h=400&fit=crop" alt="Sonr Genesis #128">
|
||||
<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">
|
||||
<wa-icon name="badge-check" variant="solid" style="color: var(--wa-color-primary); font-size: 12px;"></wa-icon>
|
||||
<span class="nft-collection-name">Sonr Genesis</span>
|
||||
</div>
|
||||
<div class="nft-name">Sonr Genesis #128</div>
|
||||
<div class="nft-price-row">
|
||||
<span class="nft-floor">Floor: 0.8 ETH</span>
|
||||
<span class="nft-value">0.85 ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wa-card>
|
||||
</main>
|
||||
</div>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (this.getAttribute('href') === '#') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.nft-card').forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
if (e.target.closest('wa-icon-button')) return;
|
||||
console.log('Open NFT detail view');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
680
_migrate/register.html
Normal file
680
_migrate/register.html
Normal file
@@ -0,0 +1,680 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: register.html → views/register.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- WebAuthn registration flow with 3-step wizard
|
||||
- Step 1: Device capability detection + auth method selection
|
||||
- Step 2: Method-specific registration (passkey, security key, or QR)
|
||||
- Step 3: Success with recovery key display and confirmation
|
||||
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
|
||||
|
||||
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
|
||||
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
|
||||
- Card max-width: 45ch (~360px) fits all viewport sizes
|
||||
- Capability list items stack vertically with compact padding
|
||||
- QR code centered with fallback text input below
|
||||
- Recovery key inputs scrollable if needed on step 3
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ RegisterPage(step int, method string, caps DeviceCapabilities) {
|
||||
@layouts.CenteredCard("Register - Sonr Motr Wallet") {
|
||||
@ProgressDots(step, 3)
|
||||
switch step {
|
||||
case 1:
|
||||
@DeviceDetectionStep(caps)
|
||||
case 2:
|
||||
@RegistrationMethodStep(method)
|
||||
case 3:
|
||||
@RegistrationSuccessStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Device detection: hx-get="/api/device/capabilities" hx-trigger="load" hx-target="#capabilities"
|
||||
- Method selection: hx-post="/register/select-method" hx-vals='{"method":"passkey"}'
|
||||
- Step navigation: hx-get="/register?step=2&method=passkey" hx-target="#step-content"
|
||||
- WebAuthn trigger: hx-post="/api/auth/register" hx-trigger="click" (JS bridge needed)
|
||||
- QR verification: hx-post="/api/auth/verify-code" hx-include="[name='code']"
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- ProgressDots(currentStep int, totalSteps int)
|
||||
- CapabilityItem(id string, label string, supported bool, loading bool)
|
||||
- RegistrationForm(method string) // passkey, security-key, or qr-code
|
||||
- RecoveryKeyDisplay(publicKey string, recoveryKey string)
|
||||
- QRCodeRegistration(qrValue string, sessionToken string)
|
||||
|
||||
STATE/PROPS:
|
||||
type DeviceCapabilities struct {
|
||||
PlatformAuth bool // Biometrics available
|
||||
CrossPlatform bool // Security key support
|
||||
ConditionalUI bool // Passkey autofill
|
||||
}
|
||||
|
||||
type RegisterState struct {
|
||||
Step int
|
||||
SelectedMethod string
|
||||
Username string
|
||||
DisplayName string
|
||||
PublicKey string
|
||||
RecoveryKey string
|
||||
Error string
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Capability detection on page load
|
||||
<div id="capabilities" hx-get="/api/device/capabilities" hx-trigger="load delay:500ms">
|
||||
@CapabilityItem("platform", "Biometric Authentication", false, true)
|
||||
@CapabilityItem("cross-platform", "Security Key", false, true)
|
||||
@CapabilityItem("conditional", "Passkey Autofill", false, true)
|
||||
</div>
|
||||
|
||||
// Radio group with HTMX method selection
|
||||
<wa-radio-group hx-on:wa-change="htmx.ajax('POST', '/register/select-method', {values: {method: event.target.value}})">
|
||||
|
||||
// Registration form submission (requires JS bridge for WebAuthn)
|
||||
<form hx-post="/api/auth/register"
|
||||
hx-target="#step-content"
|
||||
hx-indicator="#register-spinner">
|
||||
|
||||
// Recovery key confirmation checkbox enabling finish button
|
||||
<wa-checkbox hx-on:wa-change="document.getElementById('btn-finish').disabled = !this.checked">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create Account - Sonr</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
padding: var(--wa-space-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-centered wa-card {
|
||||
width: 100%;
|
||||
max-width: 45ch;
|
||||
}
|
||||
|
||||
/* Pagination steps */
|
||||
.step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Device capability badges */
|
||||
.capability-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-s);
|
||||
}
|
||||
|
||||
.capability-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s);
|
||||
border-radius: var(--wa-radius-m);
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.capability-item.supported {
|
||||
border-left: 3px solid var(--wa-color-success);
|
||||
}
|
||||
|
||||
.capability-item.unsupported {
|
||||
border-left: 3px solid var(--wa-color-danger);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Progress indicator */
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--wa-space-xs);
|
||||
margin-bottom: var(--wa-space-l);
|
||||
}
|
||||
|
||||
.progress-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--wa-color-neutral-300);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.progress-dot.active {
|
||||
background: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.progress-dot.completed {
|
||||
background: var(--wa-color-success);
|
||||
}
|
||||
|
||||
/* Viewport constraints for popup/webview (360-480px width) */
|
||||
@media (max-width: 400px) {
|
||||
.main-centered {
|
||||
padding: var(--wa-space-m);
|
||||
}
|
||||
.main-centered wa-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
.capability-item {
|
||||
padding: var(--wa-space-xs);
|
||||
gap: var(--wa-space-xs);
|
||||
font-size: var(--wa-font-size-s);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 600px) {
|
||||
.progress-steps {
|
||||
margin-bottom: var(--wa-space-s);
|
||||
}
|
||||
.capability-item {
|
||||
padding: var(--wa-space-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<main class="main-centered">
|
||||
<wa-card>
|
||||
<div slot="header">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-dot active" data-step="1"></div>
|
||||
<div class="progress-dot" data-step="2"></div>
|
||||
<div class="progress-dot" data-step="3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Device Detection -->
|
||||
<div class="step active" data-step="1">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<h2 class="wa-heading-l">Create Your Account</h2>
|
||||
<p class="wa-caption-s">
|
||||
Setting up secure sign-in...
|
||||
</p>
|
||||
|
||||
<div class="capability-list" id="capabilities">
|
||||
<div class="capability-item" id="cap-platform">
|
||||
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
|
||||
<span>Face or Fingerprint</span>
|
||||
</div>
|
||||
<div class="capability-item" id="cap-cross-platform">
|
||||
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
|
||||
<span>Hardware Security Key</span>
|
||||
</div>
|
||||
<div class="capability-item" id="cap-conditional">
|
||||
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
|
||||
<span>Quick Sign-in</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<wa-radio-group
|
||||
label="How do you want to sign in?"
|
||||
orientation="vertical"
|
||||
name="auth-method"
|
||||
id="auth-method-group"
|
||||
>
|
||||
<wa-radio appearance="button" value="passkey" id="radio-passkey" disabled>
|
||||
Use Face or Fingerprint (Recommended)
|
||||
</wa-radio>
|
||||
<wa-radio appearance="button" value="security-key" id="radio-security-key" disabled>
|
||||
Use a Hardware Key
|
||||
</wa-radio>
|
||||
<wa-radio appearance="button" value="qr-code" id="radio-qr">
|
||||
Use Another Device
|
||||
</wa-radio>
|
||||
</wa-radio-group>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button id="btn-next-1" variant="brand" disabled>
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2a: Passkey Registration -->
|
||||
<div class="step" data-step="2" data-method="passkey">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<h2 class="wa-heading-l">Set Up Face or Fingerprint</h2>
|
||||
<p class="wa-caption-s">
|
||||
Sign in securely using your face or fingerprint.
|
||||
</p>
|
||||
|
||||
<wa-input
|
||||
name="username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
required
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
|
||||
</wa-input>
|
||||
|
||||
<wa-input
|
||||
name="display-name"
|
||||
type="text"
|
||||
label="Display Name"
|
||||
placeholder="Your name (optional)"
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="id-card"></wa-icon>
|
||||
</wa-input>
|
||||
|
||||
<wa-alert variant="info" open>
|
||||
<wa-icon slot="icon" name="fingerprint"></wa-icon>
|
||||
<strong>Next: Verify your identity</strong><br>
|
||||
Your device will ask for your face, fingerprint, or PIN.
|
||||
</wa-alert>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button variant="brand" onclick="registerPasskey()">
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="fingerprint"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2b: Security Key Registration -->
|
||||
<div class="step" data-step="2" data-method="security-key">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<h2 class="wa-heading-l">Set Up Hardware Key</h2>
|
||||
<p class="wa-caption-s">
|
||||
Use a physical security key like YubiKey to sign in.
|
||||
</p>
|
||||
|
||||
<wa-input
|
||||
name="username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
required
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
|
||||
</wa-input>
|
||||
|
||||
<wa-alert variant="info" open>
|
||||
<wa-icon slot="icon" name="key"></wa-icon>
|
||||
<strong>Plug in your security key</strong><br>
|
||||
After clicking Continue, tap the button on your key when it blinks.
|
||||
</wa-alert>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button variant="brand" onclick="registerSecurityKey()">
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="key"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2c: QR Code Registration (Fallback) -->
|
||||
<div class="step" data-step="2" data-method="qr-code">
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<h2 class="wa-heading-l">Use Your Phone</h2>
|
||||
<p class="wa-caption-s">Scan this code with your phone camera to continue setup</p>
|
||||
|
||||
<wa-qr-code
|
||||
value="https://sonr.id/register?token=abc123xyz789"
|
||||
label="Scan with your phone camera"
|
||||
></wa-qr-code>
|
||||
|
||||
<p class="wa-caption-s">Or copy this link to your phone</p>
|
||||
<wa-input
|
||||
value="sonr.id/r/abc123xyz789"
|
||||
disabled
|
||||
style="width: 100%;"
|
||||
>
|
||||
<wa-copy-button slot="end" value="sonr.id/r/abc123xyz789"></wa-copy-button>
|
||||
</wa-input>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<h3 class="wa-heading-m">Enter Code from Phone</h3>
|
||||
<p class="wa-caption-s">After scanning, enter the 6-digit code shown on your phone.</p>
|
||||
|
||||
<wa-input
|
||||
type="text"
|
||||
placeholder="000000"
|
||||
maxlength="6"
|
||||
style="text-align: center; font-size: 2rem; letter-spacing: 0.5em; max-width: 200px;"
|
||||
id="verification-code"
|
||||
></wa-input>
|
||||
|
||||
<div class="wa-cluster wa-justify-content-center wa-gap-s" style="width: 100%;">
|
||||
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button variant="brand" onclick="verifyCode()">
|
||||
Verify
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<div class="step" data-step="3">
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<wa-icon name="circle-check" family="duotone" style="font-size: 4rem; color: var(--wa-color-success);"></wa-icon>
|
||||
<h2 class="wa-heading-l">You're All Set</h2>
|
||||
<p class="wa-caption-s">
|
||||
Your account is ready. Save your backup codes before continuing.
|
||||
</p>
|
||||
|
||||
<wa-alert variant="warning" open>
|
||||
<wa-icon slot="icon" name="shield-exclamation"></wa-icon>
|
||||
<strong>Important: Save these codes now</strong><br>
|
||||
If you lose your device, these codes are the only way to recover your account.
|
||||
</wa-alert>
|
||||
|
||||
<div class="wa-stack" style="width: 100%;">
|
||||
<wa-input label="Account ID" value="pk_live_a1b2c3d4e5f6g7h8i9j0" disabled>
|
||||
<wa-copy-button slot="end" value="pk_live_a1b2c3d4e5f6g7h8i9j0"></wa-copy-button>
|
||||
</wa-input>
|
||||
<wa-input label="Backup Code" value="rk_sec_z9y8x7w6v5u4t3s2r1q0" disabled>
|
||||
<wa-copy-button slot="end" value="rk_sec_z9y8x7w6v5u4t3s2r1q0"></wa-copy-button>
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
<wa-checkbox id="confirm-saved" required>
|
||||
I saved these codes somewhere safe
|
||||
</wa-checkbox>
|
||||
|
||||
<wa-button variant="brand" id="btn-finish" disabled style="width: 100%;">
|
||||
Go to Dashboard
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer slot="footer" class="wa-cluster wa-justify-content-center wa-gap-m">
|
||||
<span class="wa-caption-s">Already have an account?</span>
|
||||
<wa-button appearance="plain" size="small" onclick="window.location.href='login.html'">Sign In</wa-button>
|
||||
</footer>
|
||||
</wa-card>
|
||||
</main>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let currentStep = 1;
|
||||
let selectedMethod = null;
|
||||
let capabilities = {
|
||||
platform: false,
|
||||
crossPlatform: false,
|
||||
conditional: false
|
||||
};
|
||||
|
||||
// Check WebAuthn support
|
||||
async function checkCapabilities() {
|
||||
const capPlatform = document.getElementById('cap-platform');
|
||||
const capCrossPlatform = document.getElementById('cap-cross-platform');
|
||||
const capConditional = document.getElementById('cap-conditional');
|
||||
|
||||
// Check if WebAuthn is available at all
|
||||
if (!window.PublicKeyCredential) {
|
||||
markCapability(capPlatform, false);
|
||||
markCapability(capCrossPlatform, false);
|
||||
markCapability(capConditional, false);
|
||||
enableMethodSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check platform authenticator (biometrics)
|
||||
try {
|
||||
capabilities.platform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
markCapability(capPlatform, capabilities.platform);
|
||||
} catch (e) {
|
||||
markCapability(capPlatform, false);
|
||||
}
|
||||
|
||||
// Cross-platform (security keys) - generally available if WebAuthn exists
|
||||
capabilities.crossPlatform = true;
|
||||
markCapability(capCrossPlatform, true);
|
||||
|
||||
// Check conditional UI (passkey autofill)
|
||||
try {
|
||||
if (PublicKeyCredential.isConditionalMediationAvailable) {
|
||||
capabilities.conditional = await PublicKeyCredential.isConditionalMediationAvailable();
|
||||
}
|
||||
markCapability(capConditional, capabilities.conditional);
|
||||
} catch (e) {
|
||||
markCapability(capConditional, false);
|
||||
}
|
||||
|
||||
enableMethodSelection();
|
||||
}
|
||||
|
||||
function markCapability(element, supported) {
|
||||
const spinner = element.querySelector('wa-spinner');
|
||||
if (spinner) {
|
||||
const icon = document.createElement('wa-icon');
|
||||
icon.setAttribute('name', supported ? 'circle-check' : 'circle-xmark');
|
||||
icon.style.color = supported ? 'var(--wa-color-success)' : 'var(--wa-color-danger)';
|
||||
spinner.replaceWith(icon);
|
||||
}
|
||||
element.classList.add(supported ? 'supported' : 'unsupported');
|
||||
}
|
||||
|
||||
function enableMethodSelection() {
|
||||
const radioPasskey = document.getElementById('radio-passkey');
|
||||
const radioSecurityKey = document.getElementById('radio-security-key');
|
||||
const radioQr = document.getElementById('radio-qr');
|
||||
const btnNext = document.getElementById('btn-next-1');
|
||||
const methodGroup = document.getElementById('auth-method-group');
|
||||
|
||||
// Enable options based on capabilities
|
||||
if (capabilities.platform) {
|
||||
radioPasskey.removeAttribute('disabled');
|
||||
}
|
||||
if (capabilities.crossPlatform) {
|
||||
radioSecurityKey.removeAttribute('disabled');
|
||||
}
|
||||
// QR is always available as fallback
|
||||
|
||||
// Auto-select best available option
|
||||
if (capabilities.platform) {
|
||||
methodGroup.value = 'passkey';
|
||||
selectedMethod = 'passkey';
|
||||
} else if (capabilities.crossPlatform) {
|
||||
methodGroup.value = 'security-key';
|
||||
selectedMethod = 'security-key';
|
||||
} else {
|
||||
methodGroup.value = 'qr-code';
|
||||
selectedMethod = 'qr-code';
|
||||
}
|
||||
|
||||
btnNext.removeAttribute('disabled');
|
||||
|
||||
// Listen for method changes
|
||||
methodGroup.addEventListener('wa-change', (e) => {
|
||||
selectedMethod = e.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
function goToStep(step) {
|
||||
// Hide all steps
|
||||
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
|
||||
|
||||
// Update progress dots
|
||||
document.querySelectorAll('.progress-dot').forEach(dot => {
|
||||
const dotStep = parseInt(dot.dataset.step);
|
||||
dot.classList.remove('active', 'completed');
|
||||
if (dotStep < step) {
|
||||
dot.classList.add('completed');
|
||||
} else if (dotStep === step) {
|
||||
dot.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Show appropriate step
|
||||
if (step === 2 && selectedMethod) {
|
||||
const methodStep = document.querySelector(`.step[data-step="2"][data-method="${selectedMethod}"]`);
|
||||
if (methodStep) {
|
||||
methodStep.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
const targetStep = document.querySelector(`.step[data-step="${step}"]:not([data-method])`);
|
||||
if (targetStep) {
|
||||
targetStep.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
currentStep = step;
|
||||
}
|
||||
|
||||
// Registration functions (simulated)
|
||||
async function registerPasskey() {
|
||||
try {
|
||||
// In production, get challenge from server
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const createOptions = {
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rp: {
|
||||
name: "Sonr",
|
||||
id: window.location.hostname
|
||||
},
|
||||
user: {
|
||||
id: new Uint8Array(16),
|
||||
name: document.querySelector('[name="username"]').value || "user@example.com",
|
||||
displayName: document.querySelector('[name="display-name"]').value || "User"
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 }, // ES256
|
||||
{ type: "public-key", alg: -257 } // RS256
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform",
|
||||
userVerification: "required",
|
||||
residentKey: "required"
|
||||
},
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create(createOptions);
|
||||
console.log("Passkey created:", credential);
|
||||
goToStep(3);
|
||||
} catch (error) {
|
||||
console.error("Passkey registration failed:", error);
|
||||
alert("Registration failed. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
async function registerSecurityKey() {
|
||||
try {
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const createOptions = {
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rp: {
|
||||
name: "Sonr",
|
||||
id: window.location.hostname
|
||||
},
|
||||
user: {
|
||||
id: new Uint8Array(16),
|
||||
name: document.querySelector('.step[data-method="security-key"] [name="username"]').value || "user@example.com",
|
||||
displayName: "User"
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 },
|
||||
{ type: "public-key", alg: -257 }
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "cross-platform",
|
||||
userVerification: "preferred"
|
||||
},
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create(createOptions);
|
||||
console.log("Security key registered:", credential);
|
||||
goToStep(3);
|
||||
} catch (error) {
|
||||
console.error("Security key registration failed:", error);
|
||||
alert("Registration failed. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
function verifyCode() {
|
||||
const code = document.getElementById('verification-code').value;
|
||||
if (code.length === 6) {
|
||||
// In production, verify with server
|
||||
console.log("Verifying code:", code);
|
||||
goToStep(3);
|
||||
} else {
|
||||
alert("Please enter a valid 6-digit code");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check capabilities after a short delay to show loading state
|
||||
setTimeout(checkCapabilities, 1000);
|
||||
|
||||
// Next button handler
|
||||
document.getElementById('btn-next-1').addEventListener('click', () => {
|
||||
if (selectedMethod) {
|
||||
goToStep(2);
|
||||
}
|
||||
});
|
||||
|
||||
// Finish button handler
|
||||
const confirmCheckbox = document.getElementById('confirm-saved');
|
||||
const finishBtn = document.getElementById('btn-finish');
|
||||
|
||||
confirmCheckbox.addEventListener('wa-change', (e) => {
|
||||
if (e.target.checked) {
|
||||
finishBtn.removeAttribute('disabled');
|
||||
} else {
|
||||
finishBtn.setAttribute('disabled', '');
|
||||
}
|
||||
});
|
||||
|
||||
finishBtn.addEventListener('click', () => {
|
||||
window.location.href = 'dashboard.html';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1338
_migrate/service.html
Normal file
1338
_migrate/service.html
Normal file
File diff suppressed because it is too large
Load Diff
1635
_migrate/settings.html
Normal file
1635
_migrate/settings.html
Normal file
File diff suppressed because it is too large
Load Diff
1214
_migrate/tokens.html
Normal file
1214
_migrate/tokens.html
Normal file
File diff suppressed because it is too large
Load Diff
991
_migrate/transactions.html
Normal file
991
_migrate/transactions.html
Normal file
@@ -0,0 +1,991 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: transactions.html → views/transactions.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- Transaction history with date-grouped list view
|
||||
- Stats: Total Received, Total Sent, Swaps count, Gas Spent
|
||||
- Multi-filter bar: Type, Asset, Account, Date range, Search
|
||||
- Transaction rows with type icon, details, asset, amount, time, actions
|
||||
- Pagination with page size selector
|
||||
- Export functionality
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ TransactionsPage(data TransactionsData) {
|
||||
@layouts.DashboardLayout("transactions") {
|
||||
@PageHeader("Transactions", "Activity history across all accounts")
|
||||
@TransactionsStatsGrid(data.Stats)
|
||||
@TransactionsCard(data.Groups, data.Filter, data.Pagination)
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Filter dropdowns: hx-get="/transactions" hx-target="#tx-list" hx-include="[name^='filter']"
|
||||
- Date range: hx-get="/transactions?from={date}&to={date}" hx-target="#tx-list"
|
||||
- Search: hx-get="/transactions?search={query}" hx-trigger="keyup changed delay:300ms"
|
||||
- Pagination: hx-get="/transactions?page=2" hx-target="#tx-list" hx-push-url="true"
|
||||
- Page size: hx-get="/transactions?limit=25" hx-target="#tx-list"
|
||||
- Export: hx-get="/api/transactions/export?format=csv" (triggers download)
|
||||
- View on explorer: Client-side window.open()
|
||||
- Copy hash: Client-side wa-copy-button
|
||||
|
||||
SUB-COMPONENTS TO EXTRACT:
|
||||
- TransactionsStatsGrid(stats TxStats)
|
||||
- TransactionsCard(groups []TxDateGroup, filter TxFilter, pagination Pagination)
|
||||
- TransactionFilterBar(filter TxFilter)
|
||||
- DateRangeFilter(from, to time.Time)
|
||||
- TransactionDateGroup(date string, count int, txs []Transaction)
|
||||
- TransactionRow(tx Transaction)
|
||||
- TransactionIcon(txType string)
|
||||
- TransactionAssetBadge(asset string, toAsset string)
|
||||
- TransactionAmount(amount float64, usd float64, isPositive bool)
|
||||
- TransactionTime(timestamp time.Time, status string)
|
||||
- TransactionActions(hash string, explorerURL string)
|
||||
- PaginationControls(pagination Pagination)
|
||||
|
||||
STATE/PROPS:
|
||||
type TransactionsData struct {
|
||||
Stats TxStats
|
||||
Groups []TxDateGroup
|
||||
Filter TxFilter
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
type TxStats struct {
|
||||
TotalReceived float64
|
||||
ReceivedCount int
|
||||
TotalSent float64
|
||||
SentCount int
|
||||
SwapCount int
|
||||
SwapVolume float64
|
||||
TotalGasSpent float64
|
||||
}
|
||||
|
||||
type TxFilter struct {
|
||||
Type string // "send", "receive", "swap", "approve", "contract"
|
||||
Asset string
|
||||
Account string
|
||||
FromDate string
|
||||
ToDate string
|
||||
Search string
|
||||
}
|
||||
|
||||
type TxDateGroup struct {
|
||||
Date string
|
||||
Count int
|
||||
Transactions []Transaction
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
Type string // "send", "receive", "swap", "approve", "contract"
|
||||
Title string
|
||||
Description string
|
||||
Asset string
|
||||
ToAsset string // For swaps
|
||||
Amount float64
|
||||
AmountUSD float64
|
||||
IsPositive bool
|
||||
Timestamp time.Time
|
||||
Status string // "confirmed", "pending", "failed"
|
||||
Hash string
|
||||
ExplorerURL string
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int
|
||||
Limit int
|
||||
Total int
|
||||
TotalPages int
|
||||
}
|
||||
|
||||
HTMX PATTERNS:
|
||||
// Multi-filter bar with combined include
|
||||
<wa-select hx-get="/transactions"
|
||||
hx-target="#tx-list"
|
||||
hx-include="[name^='filter-']"
|
||||
name="filter-type">
|
||||
|
||||
// Search with debounce
|
||||
<wa-input hx-get="/transactions"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#tx-list"
|
||||
hx-include="[name^='filter-']"
|
||||
name="filter-search">
|
||||
|
||||
// Date range picker
|
||||
<wa-input type="date"
|
||||
hx-get="/transactions"
|
||||
hx-trigger="change"
|
||||
hx-target="#tx-list"
|
||||
hx-include="[name^='filter-']"
|
||||
name="filter-from">
|
||||
|
||||
// Pagination with page size
|
||||
<wa-select hx-get="/transactions"
|
||||
hx-target="#tx-list"
|
||||
hx-include="[name^='filter-']"
|
||||
name="limit">
|
||||
|
||||
// Page number buttons
|
||||
<button class="page-btn"
|
||||
hx-get="/transactions?page=2"
|
||||
hx-target="#tx-list"
|
||||
hx-push-url="true">
|
||||
|
||||
// Export button (non-HTMX, download)
|
||||
<wa-button onclick="window.location.href='/api/transactions/export?format=csv'">
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Transactions - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--wa-color-neutral-200);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--wa-space-s) var(--wa-space-xs);
|
||||
margin-bottom: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s) var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
text-decoration: none;
|
||||
color: var(--wa-color-neutral-700);
|
||||
font-size: var(--wa-font-size-s);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: var(--wa-color-surface-alt);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: var(--wa-space-xl);
|
||||
background: var(--wa-color-surface-alt);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
--min-column-size: 180px;
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--wa-color-surface);
|
||||
}
|
||||
|
||||
.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-xl);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.date-group-header .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-icon.contract {
|
||||
background: var(--wa-color-warning-subtle);
|
||||
color: var(--wa-color-warning);
|
||||
}
|
||||
|
||||
.tx-icon.approve {
|
||||
background: var(--wa-color-neutral-100);
|
||||
color: var(--wa-color-neutral-600);
|
||||
}
|
||||
|
||||
.tx-details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tx-type {
|
||||
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);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tx-asset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.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-time .time {
|
||||
font-size: var(--wa-font-size-s);
|
||||
}
|
||||
|
||||
.tx-time .status {
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--wa-space-3xl);
|
||||
color: var(--wa-color-neutral-500);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.tx-row {
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
.tx-asset, .tx-time {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<div class="dashboard-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
|
||||
<div class="wa-stack wa-gap-0">
|
||||
<span class="wa-heading-s">Sonr Wallet</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="accounts.html">
|
||||
<wa-icon name="wallet"></wa-icon>
|
||||
Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="transactions.html" class="active">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
Transactions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="tokens.html">
|
||||
<wa-icon name="coins"></wa-icon>
|
||||
Tokens
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="nfts.html">
|
||||
<wa-icon name="image"></wa-icon>
|
||||
NFTs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="activity.html">
|
||||
<wa-icon name="chart-line"></wa-icon>
|
||||
Activity
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
|
||||
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<a href="connections.html">
|
||||
<wa-icon name="plug"></wa-icon>
|
||||
Connections
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="device.html">
|
||||
<wa-icon name="mobile"></wa-icon>
|
||||
Devices
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="service.html">
|
||||
<wa-icon name="server"></wa-icon>
|
||||
Services
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="settings.html">
|
||||
<wa-icon name="gear"></wa-icon>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<div class="wa-flank">
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xl">Transactions</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Activity history across all connected accounts</span>
|
||||
</div>
|
||||
<div class="wa-cluster wa-gap-s">
|
||||
<wa-button variant="neutral" appearance="outlined" size="small">
|
||||
<wa-icon slot="start" name="arrow-down-to-line"></wa-icon>
|
||||
Export
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wa-grid stats-grid">
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Received</span>
|
||||
<span class="wa-heading-xl" style="color: var(--wa-color-success);">
|
||||
+<wa-format-number type="currency" currency="USD" value="15420.50" lang="en-US"></wa-format-number>
|
||||
</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">42 transactions</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Sent</span>
|
||||
<span class="wa-heading-xl" style="color: var(--wa-color-danger);">
|
||||
-<wa-format-number type="currency" currency="USD" value="8234.18" lang="en-US"></wa-format-number>
|
||||
</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">28 transactions</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Swaps</span>
|
||||
<span class="wa-heading-xl">12</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">$4,892.00 volume</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<wa-card class="stat-card">
|
||||
<div class="wa-stack wa-gap-3xs">
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Gas Spent</span>
|
||||
<span class="wa-heading-xl">
|
||||
<wa-format-number type="currency" currency="USD" value="127.45" lang="en-US"></wa-format-number>
|
||||
</span>
|
||||
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">All time</span>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
<wa-card>
|
||||
<div class="filter-bar">
|
||||
<wa-select placeholder="All Types" size="small" style="min-width: 140px;" clearable>
|
||||
<wa-option value="send">
|
||||
<wa-icon slot="prefix" name="arrow-up" style="color: var(--wa-color-danger);"></wa-icon>
|
||||
Send
|
||||
</wa-option>
|
||||
<wa-option value="receive">
|
||||
<wa-icon slot="prefix" name="arrow-down" style="color: var(--wa-color-success);"></wa-icon>
|
||||
Receive
|
||||
</wa-option>
|
||||
<wa-option value="swap">
|
||||
<wa-icon slot="prefix" name="arrow-right-arrow-left" style="color: var(--wa-color-primary);"></wa-icon>
|
||||
Swap
|
||||
</wa-option>
|
||||
<wa-option value="approve">
|
||||
<wa-icon slot="prefix" name="check"></wa-icon>
|
||||
Approve
|
||||
</wa-option>
|
||||
<wa-option value="contract">
|
||||
<wa-icon slot="prefix" name="file-code"></wa-icon>
|
||||
Contract
|
||||
</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-option value="avax">AVAX</wa-option>
|
||||
</wa-select>
|
||||
|
||||
<wa-select placeholder="All Accounts" size="small" style="min-width: 160px;" clearable>
|
||||
<wa-option value="main">Main Wallet</wa-option>
|
||||
<wa-option value="trading">Trading</wa-option>
|
||||
<wa-option value="savings">Savings</wa-option>
|
||||
</wa-select>
|
||||
|
||||
<wa-input type="date" size="small" style="width: 150px;" placeholder="From date"></wa-input>
|
||||
<span style="color: var(--wa-color-neutral-400);">—</span>
|
||||
<wa-input type="date" size="small" style="width: 150px;" placeholder="To date"></wa-input>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="date-group">
|
||||
<div class="date-group-header">
|
||||
<h3>Today</h3>
|
||||
<span class="tx-count">3 transactions</span>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon receive">
|
||||
<wa-icon name="arrow-down"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Received ETH</div>
|
||||
<div class="tx-address">From: 0x742d...35Cb</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="E" style="--size: 24px; background: #627eea;"></wa-avatar>
|
||||
<span>ETH</span>
|
||||
</div>
|
||||
<div class="tx-amount positive">
|
||||
<div class="value">+0.25 ETH</div>
|
||||
<div class="usd">$586.25</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">2:34 PM</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="0x8f4a2b1c9e3d5f7a0b2c4d6e8f0a1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon swap">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Swapped SNR → USDC</div>
|
||||
<div class="tx-address">via Uniswap V3</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="S" style="--size: 24px; background: linear-gradient(135deg, #17c2ff, #0090ff);"></wa-avatar>
|
||||
<wa-icon name="arrow-right" style="font-size: 10px; color: var(--wa-color-neutral-400);"></wa-icon>
|
||||
<wa-avatar initials="U" style="--size: 24px; background: #2775ca;"></wa-avatar>
|
||||
</div>
|
||||
<div class="tx-amount">
|
||||
<div class="value">500 SNR → 248.50 USDC</div>
|
||||
<div class="usd">$248.50</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">11:22 AM</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="0x7d3e2a1b8c4f6e0d9a5b7c3e1f8a2d4c6b0e9f7a1b2c3d4e5f6a7b8c9d0e1f2a" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon approve">
|
||||
<wa-icon name="check"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Approved USDC</div>
|
||||
<div class="tx-address">Spender: Uniswap V3 Router</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="U" style="--size: 24px; background: #2775ca;"></wa-avatar>
|
||||
<span>USDC</span>
|
||||
</div>
|
||||
<div class="tx-amount">
|
||||
<div class="value">Unlimited</div>
|
||||
<div class="usd">—</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">11:20 AM</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="0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-group">
|
||||
<div class="date-group-header">
|
||||
<h3>Yesterday</h3>
|
||||
<span class="tx-count">2 transactions</span>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon send">
|
||||
<wa-icon name="arrow-up"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Sent SNR</div>
|
||||
<div class="tx-address">To: sonr1k4m...9p3q</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="S" style="--size: 24px; background: linear-gradient(135deg, #17c2ff, #0090ff);"></wa-avatar>
|
||||
<span>SNR</span>
|
||||
</div>
|
||||
<div class="tx-amount negative">
|
||||
<div class="value">-500 SNR</div>
|
||||
<div class="usd">$250.00</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">4:18 PM</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="0x9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon receive">
|
||||
<wa-icon name="arrow-down"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Received AVAX</div>
|
||||
<div class="tx-address">From: 0xaBcD...1234</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="A" style="--size: 24px; background: #e84142;"></wa-avatar>
|
||||
<span>AVAX</span>
|
||||
</div>
|
||||
<div class="tx-amount positive">
|
||||
<div class="value">+10.00 AVAX</div>
|
||||
<div class="usd">$281.50</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">9:45 AM</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="0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-group">
|
||||
<div class="date-group-header">
|
||||
<h3>January 1, 2026</h3>
|
||||
<span class="tx-count">4 transactions</span>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon swap">
|
||||
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Swapped ETH → USDC</div>
|
||||
<div class="tx-address">via Uniswap V3</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="E" style="--size: 24px; background: #627eea;"></wa-avatar>
|
||||
<wa-icon name="arrow-right" style="font-size: 10px; color: var(--wa-color-neutral-400);"></wa-icon>
|
||||
<wa-avatar initials="U" style="--size: 24px; background: #2775ca;"></wa-avatar>
|
||||
</div>
|
||||
<div class="tx-amount">
|
||||
<div class="value">0.5 ETH → 1,172.50 USDC</div>
|
||||
<div class="usd">$1,172.50</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">9:15 AM</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="0x3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon contract">
|
||||
<wa-icon name="file-code"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Contract Interaction</div>
|
||||
<div class="tx-address">Staking Contract: 0x5f3c...8a2b</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="S" style="--size: 24px; background: linear-gradient(135deg, #17c2ff, #0090ff);"></wa-avatar>
|
||||
<span>SNR</span>
|
||||
</div>
|
||||
<div class="tx-amount negative">
|
||||
<div class="value">-1,000 SNR</div>
|
||||
<div class="usd">$500.00 staked</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">8:30 AM</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="0x4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon send">
|
||||
<wa-icon name="arrow-up"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Sent ETH</div>
|
||||
<div class="tx-address">To: 0x9f8e...7d6c</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="E" style="--size: 24px; background: #627eea;"></wa-avatar>
|
||||
<span>ETH</span>
|
||||
</div>
|
||||
<div class="tx-amount negative">
|
||||
<div class="value">-0.15 ETH</div>
|
||||
<div class="usd">$351.85</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">7:22 AM</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="0x5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon receive">
|
||||
<wa-icon name="arrow-down"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Received SNR</div>
|
||||
<div class="tx-address">From: sonr1abc...xyz9 (Airdrop)</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="S" style="--size: 24px; background: linear-gradient(135deg, #17c2ff, #0090ff);"></wa-avatar>
|
||||
<span>SNR</span>
|
||||
</div>
|
||||
<div class="tx-amount positive">
|
||||
<div class="value">+2,500 SNR</div>
|
||||
<div class="usd">$1,250.00</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">12:00 AM</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="0x6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-group">
|
||||
<div class="date-group-header">
|
||||
<h3>December 30, 2025</h3>
|
||||
<span class="tx-count">1 transaction</span>
|
||||
</div>
|
||||
|
||||
<div class="tx-row">
|
||||
<div class="tx-icon receive">
|
||||
<wa-icon name="arrow-down"></wa-icon>
|
||||
</div>
|
||||
<div class="tx-details">
|
||||
<div class="tx-type">Received AVAX</div>
|
||||
<div class="tx-address">From: 0xfEdC...bA98</div>
|
||||
</div>
|
||||
<div class="tx-asset">
|
||||
<wa-avatar initials="A" style="--size: 24px; background: #e84142;"></wa-avatar>
|
||||
<span>AVAX</span>
|
||||
</div>
|
||||
<div class="tx-amount positive">
|
||||
<div class="value">+14.83 AVAX</div>
|
||||
<div class="usd">$417.53</div>
|
||||
</div>
|
||||
<div class="tx-time">
|
||||
<div class="time">4:48 PM</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="0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b" copy-label="Copy hash"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination-info">
|
||||
Showing <strong>1-10</strong> of <strong>82</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-option value="100">100 / 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>
|
||||
</wa-card>
|
||||
</main>
|
||||
</div>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.page-btn:not(:disabled)').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.page-btn').forEach(b => b.classList.remove('active'));
|
||||
if (!this.querySelector('wa-icon')) {
|
||||
this.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (this.getAttribute('href') === '#') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
536
_migrate/welcome.html
Normal file
536
_migrate/welcome.html
Normal file
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
================================================================================
|
||||
TEMPL MIGRATION GUIDE: welcome.html → views/welcome.templ
|
||||
================================================================================
|
||||
|
||||
PAGE OVERVIEW:
|
||||
- Onboarding landing page with 3-step stepper (Welcome → Learn → Get Started)
|
||||
- Routes users to register.html or login.html
|
||||
- Network status indicator in footer
|
||||
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
|
||||
|
||||
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
|
||||
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
|
||||
- Card max-width: 48ch (~384px) fits all viewport sizes
|
||||
- Padding scales down on smaller viewports via media queries
|
||||
- Touch targets minimum 44×44px for mobile accessibility
|
||||
- Footer should remain visible without scrolling on step 1/3
|
||||
- Scrollable content area for step 2 (feature list)
|
||||
|
||||
MAIN TEMPL COMPONENT:
|
||||
templ WelcomePage() {
|
||||
@layouts.CenteredCard("Welcome - Sonr Motr Wallet") {
|
||||
@OnboardingStepper(1)
|
||||
@WelcomeStep1()
|
||||
}
|
||||
}
|
||||
|
||||
HTMX INTEGRATION:
|
||||
- Replace onclick="goToStep(N)" with hx-get="/welcome/step/N" hx-target="#step-content"
|
||||
- Stepper state managed server-side via URL params or session
|
||||
- Navigation buttons trigger HTMX partial updates
|
||||
|
||||
STATE MANAGEMENT:
|
||||
- Current step passed as prop: currentStep int
|
||||
- Step completion status tracked server-side
|
||||
================================================================================
|
||||
-->
|
||||
<html lang="en" class="wa-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Welcome - Sonr Motr Wallet</title>
|
||||
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--wa-color-primary: #17c2ff;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
padding: var(--wa-space-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-centered wa-card {
|
||||
width: 100%;
|
||||
max-width: 48ch;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.onboarding-stepper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-m) 0;
|
||||
}
|
||||
|
||||
.stepper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.stepper-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--wa-font-size-s);
|
||||
font-weight: 600;
|
||||
background: var(--wa-color-neutral-200);
|
||||
color: var(--wa-color-neutral-600);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stepper-item.active .stepper-number {
|
||||
background: var(--wa-color-primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 4px var(--wa-color-primary-subtle);
|
||||
}
|
||||
|
||||
.stepper-item.completed .stepper-number {
|
||||
background: var(--wa-color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stepper-label {
|
||||
font-size: var(--wa-font-size-xs);
|
||||
color: var(--wa-color-neutral-500);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.stepper-label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Viewport constraints for popup/webview (360-480px width) */
|
||||
@media (max-width: 400px) {
|
||||
.main-centered {
|
||||
padding: var(--wa-space-m);
|
||||
}
|
||||
.main-centered wa-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
.hero-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.hero-icon wa-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
.feature-item {
|
||||
padding: var(--wa-space-s);
|
||||
}
|
||||
.feature-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.action-card {
|
||||
padding: var(--wa-space-m);
|
||||
}
|
||||
.action-card wa-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 600px) {
|
||||
.hero-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: var(--wa-space-m);
|
||||
}
|
||||
.hero-icon wa-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.onboarding-stepper {
|
||||
padding: var(--wa-space-s) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-item.active .stepper-label {
|
||||
color: var(--wa-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stepper-line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: var(--wa-color-neutral-200);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.stepper-line.completed {
|
||||
background: var(--wa-color-success);
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--wa-radius-l);
|
||||
background: linear-gradient(135deg, var(--wa-color-primary), #0090ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto var(--wa-space-l);
|
||||
box-shadow: 0 8px 32px rgba(23, 194, 255, 0.3);
|
||||
}
|
||||
|
||||
.hero-icon wa-icon {
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--wa-space-m);
|
||||
padding: var(--wa-space-m);
|
||||
background: var(--wa-color-surface-alt);
|
||||
border-radius: var(--wa-radius-m);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--wa-radius-s);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-icon.security {
|
||||
background: var(--wa-color-success-subtle);
|
||||
color: var(--wa-color-success);
|
||||
}
|
||||
|
||||
.feature-icon.speed {
|
||||
background: var(--wa-color-primary-subtle);
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.feature-icon.privacy {
|
||||
background: var(--wa-color-warning-subtle);
|
||||
color: var(--wa-color-warning);
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-m);
|
||||
padding: var(--wa-space-xl);
|
||||
background: var(--wa-color-surface-alt);
|
||||
border-radius: var(--wa-radius-l);
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
border-color: var(--wa-color-primary);
|
||||
background: var(--wa-color-primary-subtle);
|
||||
}
|
||||
|
||||
.action-card wa-icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--wa-color-primary);
|
||||
}
|
||||
|
||||
.network-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--wa-color-success);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<wa-page>
|
||||
<main class="main-centered">
|
||||
<wa-card>
|
||||
<div slot="header">
|
||||
<div class="onboarding-stepper">
|
||||
<div class="stepper-item active" data-step="1">
|
||||
<div class="stepper-number">1</div>
|
||||
<span class="stepper-label">Welcome</span>
|
||||
</div>
|
||||
<div class="stepper-line"></div>
|
||||
<div class="stepper-item" data-step="2">
|
||||
<div class="stepper-number">2</div>
|
||||
<span class="stepper-label">Learn</span>
|
||||
</div>
|
||||
<div class="stepper-line"></div>
|
||||
<div class="stepper-item" data-step="3">
|
||||
<div class="stepper-number">3</div>
|
||||
<span class="stepper-label">Get Started</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step active" data-step="1">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="hero-icon">
|
||||
<wa-icon name="wallet" family="duotone"></wa-icon>
|
||||
</div>
|
||||
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h1 class="wa-heading-xl">Welcome to Sonr</h1>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Your self-sovereign identity wallet powered by WebAssembly
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<div class="wa-stack wa-gap-s">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon security">
|
||||
<wa-icon name="shield-check"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">Passwordless Security</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
Authenticate with biometrics or hardware keys - no passwords to remember or steal
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon speed">
|
||||
<wa-icon name="bolt"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">WASM-Powered</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
Wallet runs entirely in your browser - fast, secure, and always available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon privacy">
|
||||
<wa-icon name="user-shield"></wa-icon>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">You Own Your Data</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
Self-sovereign identity means your keys never leave your device
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-button variant="brand" size="large" style="width: 100%;" onclick="goToStep(2)">
|
||||
Learn More
|
||||
<wa-icon slot="end" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" data-step="2">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">How Motr Works</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
A next-generation wallet built on WebAuthn and WASM
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<div class="wa-stack wa-gap-m">
|
||||
<wa-callout variant="neutral">
|
||||
<wa-icon slot="icon" name="microchip"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">WebAssembly Runtime</span>
|
||||
<span class="wa-caption-s">Your wallet logic runs as compiled Go code in a secure WASM sandbox, directly in your browser.</span>
|
||||
</div>
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="neutral">
|
||||
<wa-icon slot="icon" name="fingerprint"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">Passkey Authentication</span>
|
||||
<span class="wa-caption-s">Use Face ID, Touch ID, or hardware security keys. Your biometrics stay on your device.</span>
|
||||
</div>
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="neutral">
|
||||
<wa-icon slot="icon" name="network-wired"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">Decentralized Identity</span>
|
||||
<span class="wa-caption-s">Connect to any app with OpenID Connect - you control what data to share.</span>
|
||||
</div>
|
||||
</wa-callout>
|
||||
|
||||
<wa-callout variant="neutral">
|
||||
<wa-icon slot="icon" name="key"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-xs">Multi-Chain Support</span>
|
||||
<span class="wa-caption-s">One wallet for Sonr, Ethereum, Cosmos, Bitcoin and more via IBC.</span>
|
||||
</div>
|
||||
</wa-callout>
|
||||
</div>
|
||||
|
||||
<div class="wa-cluster wa-gap-s" style="justify-content: space-between;">
|
||||
<wa-button variant="neutral" appearance="outlined" onclick="goToStep(1)">
|
||||
<wa-icon slot="start" name="arrow-left"></wa-icon>
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button variant="brand" onclick="goToStep(3)">
|
||||
Get Started
|
||||
<wa-icon slot="end" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" data-step="3">
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">Ready to Begin?</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Create a new wallet or sign in to an existing one
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<div class="wa-grid" style="--min-column-size: 180px; gap: var(--wa-space-m);">
|
||||
<div class="action-card" onclick="window.location.href='register.html'">
|
||||
<wa-icon name="user-plus" family="duotone"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-m">Create Wallet</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
New to Sonr? Set up your wallet in minutes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card" onclick="window.location.href='login.html'">
|
||||
<wa-icon name="right-to-bracket" family="duotone"></wa-icon>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
<span class="wa-heading-m">Sign In</span>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
Access your existing wallet
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-divider>or</wa-divider>
|
||||
|
||||
<wa-button variant="neutral" appearance="outlined" style="width: 100%;" onclick="scanQR()">
|
||||
<wa-icon slot="start" name="qrcode"></wa-icon>
|
||||
Scan QR Code
|
||||
</wa-button>
|
||||
|
||||
<wa-callout variant="brand" appearance="filled">
|
||||
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
||||
<span class="wa-caption-s">
|
||||
Already have a passkey registered on another device? Use QR code to sync your wallet.
|
||||
</span>
|
||||
</wa-callout>
|
||||
|
||||
<wa-button variant="neutral" appearance="plain" onclick="goToStep(2)" style="width: 100%;">
|
||||
<wa-icon slot="start" name="arrow-left"></wa-icon>
|
||||
Back to How It Works
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer slot="footer">
|
||||
<div class="wa-stack wa-gap-m wa-align-items-center">
|
||||
<div class="network-status">
|
||||
<div class="status-dot"></div>
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
|
||||
Sonr Network: <strong style="color: var(--wa-color-success);">Operational</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wa-cluster wa-justify-content-center wa-gap-m">
|
||||
<wa-button appearance="plain" size="small" onclick="window.open('https://sonr.io', '_blank')">
|
||||
Learn about Sonr
|
||||
</wa-button>
|
||||
<span style="color: var(--wa-color-neutral-300);">|</span>
|
||||
<wa-button appearance="plain" size="small" onclick="window.open('https://docs.sonr.io', '_blank')">
|
||||
Documentation
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</wa-card>
|
||||
</main>
|
||||
</wa-page>
|
||||
|
||||
<script>
|
||||
let currentStep = 1;
|
||||
|
||||
function goToStep(step) {
|
||||
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
|
||||
|
||||
document.querySelectorAll('.stepper-item').forEach(item => {
|
||||
const itemStep = parseInt(item.dataset.step);
|
||||
item.classList.remove('active', 'completed');
|
||||
if (itemStep < step) {
|
||||
item.classList.add('completed');
|
||||
} else if (itemStep === step) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.stepper-line').forEach((line, index) => {
|
||||
line.classList.toggle('completed', index < step - 1);
|
||||
});
|
||||
|
||||
const targetStep = document.querySelector(`.step[data-step="${step}"]`);
|
||||
if (targetStep) {
|
||||
targetStep.classList.add('active');
|
||||
}
|
||||
|
||||
currentStep = step;
|
||||
}
|
||||
|
||||
function scanQR() {
|
||||
alert('QR Scanner would open here - scan a code from another device to sync your wallet.');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user