chore(init): Setup Makefile for build and test automation

This commit is contained in:
2026-01-05 13:28:43 -05:00
parent 88d521f13c
commit 8b31e5c54b
18 changed files with 18284 additions and 0 deletions

13
Makefile Normal file
View 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

File diff suppressed because it is too large Load Diff

882
_migrate/activity.html Normal file
View 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

File diff suppressed because it is too large Load Diff

719
_migrate/authorize.html Normal file
View 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

File diff suppressed because it is too large Load Diff

860
_migrate/connections.html Normal file
View 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

File diff suppressed because it is too large Load Diff

654
_migrate/demo.html Normal file
View 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

File diff suppressed because it is too large Load Diff

561
_migrate/login.html Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1635
_migrate/settings.html Normal file

File diff suppressed because it is too large Load Diff

1214
_migrate/tokens.html Normal file

File diff suppressed because it is too large Load Diff

991
_migrate/transactions.html Normal file
View 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
View 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>