Web3 — Wallet Dashboard (balances · tokens · NFTs)
A premium, dark-first Web3 wallet dashboard with a glassy address chip, network switcher, and a gradient-bordered hero card showing an animated total balance and 24h PnL. Send, Receive, Swap, and Buy actions sit beside a portfolio allocation donut, while Tokens, NFTs, and Activity tabs reveal token rows with live prices, CSS-drawn NFT thumbnails, and a transaction feed. Includes a hide-balances eye toggle, a guarded swap modal with risk confirmation, and toasts. All mock data, fictional tokens.
MCP
Kod
:root {
--bg: #0a0b0f;
--surface: #13151c;
--surface-2: #1b1e27;
--elevated: #23262f;
--text: #e9ecf2;
--muted: #8a90a2;
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--accent: #7c5cff;
--accent-2: #00e0c6;
--accent-glow: rgba(124, 92, 255, 0.45);
--pos: #26d07c;
--neg: #ff4d6d;
--warn: #ffb347;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
--net-lumen: #7c5cff;
--net-nebula: #00e0c6;
--net-aurora: #ffb347;
--net-solara: #ff6ad5;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
body {
background: var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-feature-settings: "tnum" 1;
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.app {
position: relative;
max-width: 920px;
margin: 0 auto;
padding: 18px clamp(14px, 4vw, 26px) 60px;
}
.app::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(60% 50% at 18% 0%, rgba(124, 92, 255, 0.16), transparent 70%),
radial-gradient(50% 40% at 92% 8%, rgba(0, 224, 198, 0.1), transparent 70%);
pointer-events: none;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 22px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 17px;
}
.brand-mark {
width: 26px;
height: 26px;
border-radius: 8px;
background: conic-gradient(from 210deg, var(--accent), var(--accent-2), var(--accent));
box-shadow: 0 0 0 1px var(--line-2), 0 6px 18px var(--accent-glow);
}
.brand-dim {
color: var(--muted);
font-weight: 500;
}
.topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
/* Network switcher */
.net-switch {
position: relative;
}
.net-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 11px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(10px);
color: var(--text);
border-radius: var(--r-pill);
font-size: 13.5px;
font-weight: 500;
transition: border-color 0.18s, background 0.18s;
}
.net-btn:hover {
border-color: var(--line-2);
background: rgba(255, 255, 255, 0.07);
}
.net-dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex: none;
}
.net-dot[data-net="lumen"] { background: var(--net-lumen); box-shadow: 0 0 8px var(--net-lumen); }
.net-dot[data-net="nebula"] { background: var(--net-nebula); box-shadow: 0 0 8px var(--net-nebula); }
.net-dot[data-net="aurora"] { background: var(--net-aurora); box-shadow: 0 0 8px var(--net-aurora); }
.net-dot[data-net="solara"] { background: var(--net-solara); box-shadow: 0 0 8px var(--net-solara); }
.chev {
width: 14px;
height: 14px;
color: var(--muted);
transition: transform 0.2s;
}
.net-btn[aria-expanded="true"] .chev {
transform: rotate(180deg);
}
.net-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 210px;
list-style: none;
margin: 0;
padding: 6px;
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
backdrop-filter: blur(14px);
z-index: 30;
animation: pop 0.16s ease;
}
@keyframes pop {
from { opacity: 0; transform: translateY(-6px) scale(0.98); }
}
.net-menu li {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 10px;
border-radius: var(--r-sm);
font-size: 13.5px;
cursor: pointer;
transition: background 0.14s;
}
.net-menu li:hover,
.net-menu li:focus-visible {
background: rgba(255, 255, 255, 0.06);
outline: none;
}
.net-menu li.is-active {
background: rgba(124, 92, 255, 0.12);
}
.net-meta {
margin-left: auto;
font-size: 10.5px;
color: var(--muted);
padding: 2px 6px;
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
/* Address chip */
.addr-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 11px 7px 8px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.04);
border-radius: var(--r-pill);
color: var(--text);
font-size: 13px;
transition: border-color 0.18s, background 0.18s;
}
.addr-chip:hover {
border-color: var(--line-2);
background: rgba(255, 255, 255, 0.07);
}
.addr-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
background: conic-gradient(from 90deg, #ff6ad5, var(--accent), var(--accent-2), #ff6ad5);
flex: none;
}
.copy-ico {
width: 14px;
height: 14px;
color: var(--muted);
}
/* ---------- Cards ---------- */
.card {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.01));
border: 1px solid var(--line);
border-radius: var(--r-lg);
backdrop-filter: blur(8px);
}
.main {
display: grid;
grid-template-columns: 1.45fr 1fr;
gap: 16px;
}
/* ---------- Hero ---------- */
.hero {
grid-column: 1 / -1;
position: relative;
padding: 22px 24px 20px;
border-radius: var(--r-lg);
background:
radial-gradient(120% 120% at 0% 0%, rgba(124, 92, 255, 0.16), transparent 55%),
linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid transparent;
background-clip: padding-box;
overflow: hidden;
}
.hero::before {
content: "";
position: absolute;
inset: 0;
padding: 1px;
border-radius: inherit;
background: linear-gradient(135deg, var(--accent), transparent 40%, var(--accent-2));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0.55;
pointer-events: none;
}
.hero-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.hero-label {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
letter-spacing: 0.02em;
}
.eye-btn {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
border-radius: var(--r-sm);
color: var(--muted);
transition: color 0.16s, border-color 0.16s;
}
.eye-btn:hover { color: var(--text); border-color: var(--line-2); }
.eye-btn svg { width: 16px; height: 16px; }
.eye-btn .eye-off { display: none; }
.eye-btn[aria-pressed="true"] .eye-open { display: none; }
.eye-btn[aria-pressed="true"] .eye-off { display: block; }
.hero-chain {
font-size: 11.5px;
color: var(--muted);
padding: 4px 9px;
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
.hero-value {
display: flex;
align-items: baseline;
margin: 12px 0 2px;
font-weight: 600;
}
.hero-cur {
font-size: 26px;
color: var(--muted);
margin-right: 2px;
}
.hero-num {
font-size: clamp(38px, 8vw, 54px);
letter-spacing: -0.02em;
line-height: 1;
}
.hero-decimals {
font-size: clamp(22px, 4vw, 30px);
color: var(--muted);
align-self: flex-start;
margin-top: 4px;
}
.is-hidden .hero-num,
.is-hidden .hero-decimals,
.is-hidden .hero-cur {
color: transparent;
text-shadow: 0 0 14px rgba(233, 236, 242, 0.5);
}
.hero-sub {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 18px;
}
.pnl {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13.5px;
font-weight: 500;
padding: 4px 10px;
border-radius: var(--r-pill);
}
.pnl svg { width: 11px; height: 11px; }
.pnl-pos { color: var(--pos); background: rgba(38, 208, 124, 0.12); }
.pnl-neg { color: var(--neg); background: rgba(255, 77, 109, 0.12); }
.pnl-neg svg { transform: rotate(180deg); }
.pnl-pct { opacity: 0.85; }
.hero-period {
color: var(--muted);
font-size: 12.5px;
}
.hero-actions {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.act {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 13px 6px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.04);
border-radius: var(--r-md);
color: var(--text);
font-size: 12.5px;
font-weight: 500;
transition: transform 0.16s, border-color 0.16s, background 0.16s, box-shadow 0.16s;
}
.act svg { width: 20px; height: 20px; }
.act:hover {
transform: translateY(-2px);
border-color: var(--line-2);
background: rgba(255, 255, 255, 0.07);
}
.act:active { transform: translateY(0); }
.act-primary {
background: linear-gradient(135deg, var(--accent), #6a4cff);
border-color: transparent;
box-shadow: 0 8px 24px var(--accent-glow);
}
.act-primary:hover {
background: linear-gradient(135deg, #8a6cff, var(--accent));
box-shadow: 0 10px 30px var(--accent-glow);
}
/* ---------- Allocation ---------- */
.alloc {
display: flex;
align-items: center;
gap: 18px;
padding: 20px;
}
.alloc-donut {
position: relative;
width: 116px;
height: 116px;
flex: none;
}
.donut {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.donut circle {
fill: none;
stroke-width: 13;
}
.donut-track {
stroke: rgba(255, 255, 255, 0.06);
}
.donut-seg {
stroke-linecap: round;
stroke-dasharray: 0 999;
transition: stroke-dasharray 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
.donut-seg[data-seg="0"] { stroke: var(--accent); }
.donut-seg[data-seg="1"] { stroke: var(--accent-2); }
.donut-seg[data-seg="2"] { stroke: var(--warn); }
.donut-seg[data-seg="3"] { stroke: #ff6ad5; }
.donut-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
}
.donut-k { font-size: 11px; color: var(--muted); }
.donut-v { font-size: 22px; font-weight: 600; }
.alloc-legend {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
display: grid;
gap: 9px;
}
.alloc-legend li {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.lg-dot {
width: 9px;
height: 9px;
border-radius: 3px;
flex: none;
}
.lg-name { font-weight: 500; }
.lg-pct { margin-left: auto; color: var(--muted); }
/* ---------- Tabs panel ---------- */
.panel {
grid-column: 1 / -1;
padding: 6px;
}
.tabs {
position: relative;
display: flex;
gap: 4px;
padding: 4px;
border-bottom: 1px solid var(--line);
}
.tab {
border: 0;
background: transparent;
color: var(--muted);
font-size: 14px;
font-weight: 500;
padding: 12px 16px;
border-radius: var(--r-sm);
transition: color 0.16s;
}
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--text); }
.tab-badge {
font-size: 10.5px;
padding: 1px 6px;
border-radius: var(--r-pill);
background: rgba(124, 92, 255, 0.18);
color: #cdbcff;
margin-left: 4px;
}
.tab-ink {
position: absolute;
bottom: -1px;
height: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
border-radius: 2px;
transition: left 0.28s cubic-bezier(0.22, 1, 0.36, 1), width 0.28s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: 0 0 10px var(--accent-glow);
}
.tabpanel { display: none; padding: 8px; }
.tabpanel.is-active { display: block; animation: fade 0.25s ease; }
@keyframes fade {
from { opacity: 0; transform: translateY(6px); }
}
/* Tokens */
.token-head {
display: grid;
grid-template-columns: 2fr 1.2fr 1.2fr 1fr;
padding: 8px 12px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.th-price, .th-bal { text-align: right; }
.th-val { text-align: right; }
.token-list { list-style: none; margin: 0; padding: 0; }
.token-row {
display: grid;
grid-template-columns: 2fr 1.2fr 1.2fr 1fr;
align-items: center;
padding: 12px;
border-radius: var(--r-md);
transition: background 0.14s;
cursor: pointer;
}
.token-row:hover { background: rgba(255, 255, 255, 0.04); }
.tk-asset { display: flex; align-items: center; gap: 11px; }
.tk-logo {
width: 34px;
height: 34px;
border-radius: 50%;
flex: none;
display: grid;
place-items: center;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
font-weight: 700;
color: #0a0b0f;
}
.tk-name { font-weight: 600; font-size: 14px; }
.tk-full { font-size: 11.5px; color: var(--muted); }
.tk-price { text-align: right; }
.tk-price-v { font-size: 13.5px; }
.tk-chg { font-size: 11.5px; }
.tk-chg.up { color: var(--pos); }
.tk-chg.down { color: var(--neg); }
.tk-bal { text-align: right; font-size: 13.5px; }
.tk-bal-sub { font-size: 11px; color: var(--muted); }
.tk-val { text-align: right; font-weight: 600; font-size: 14px; }
.is-hidden .tk-bal,
.is-hidden .tk-val,
.is-hidden .donut-v,
.is-hidden .lg-pct {
filter: blur(7px);
user-select: none;
}
/* NFTs */
.nft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
padding: 8px 4px;
}
.nft-card {
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface);
transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
cursor: pointer;
}
.nft-card:hover {
transform: translateY(-3px);
border-color: var(--line-2);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.45);
}
.nft-art {
aspect-ratio: 1;
position: relative;
overflow: hidden;
}
.nft-meta { padding: 9px 11px 11px; }
.nft-name { font-size: 13px; font-weight: 600; }
.nft-coll { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 4px; }
.nft-price {
display: flex;
align-items: baseline;
gap: 5px;
margin-top: 6px;
font-size: 12.5px;
}
.nft-price b { font-weight: 600; }
.nft-floor { color: var(--muted); margin-left: auto; font-size: 11px; }
/* Activity */
.act-list { list-style: none; margin: 0; padding: 0; }
.act-item {
display: flex;
align-items: center;
gap: 13px;
padding: 13px 12px;
border-bottom: 1px solid var(--line);
}
.act-item:last-child { border-bottom: 0; }
.act-ico {
width: 36px;
height: 36px;
border-radius: 12px;
display: grid;
place-items: center;
flex: none;
background: rgba(255, 255, 255, 0.05);
color: var(--muted);
}
.act-ico svg { width: 18px; height: 18px; }
.act-ico.send { color: var(--neg); background: rgba(255, 77, 109, 0.1); }
.act-ico.recv { color: var(--pos); background: rgba(38, 208, 124, 0.1); }
.act-ico.swap { color: var(--accent-2); background: rgba(0, 224, 198, 0.1); }
.act-ico.mint { color: #ff6ad5; background: rgba(255, 106, 213, 0.1); }
.act-body { flex: 1; min-width: 0; }
.act-title { font-size: 14px; font-weight: 500; }
.act-sub { font-size: 11.5px; color: var(--muted); display: flex; gap: 8px; align-items: center; }
.act-amt { text-align: right; font-size: 13.5px; }
.act-amt.pos { color: var(--pos); }
.act-amt.neg { color: var(--text); }
.act-status {
font-size: 10.5px;
padding: 3px 8px;
border-radius: var(--r-pill);
font-weight: 500;
}
.act-status.confirmed { color: var(--pos); background: rgba(38, 208, 124, 0.12); }
.act-status.pending { color: var(--warn); background: rgba(255, 179, 71, 0.12); }
.act-status.failed { color: var(--neg); background: rgba(255, 77, 109, 0.12); }
.is-hidden .act-amt { filter: blur(6px); }
/* ---------- Modal ---------- */
.modal {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 18px;
}
.modal-scrim {
position: absolute;
inset: 0;
background: rgba(5, 6, 9, 0.66);
backdrop-filter: blur(4px);
animation: fade 0.2s ease;
}
.modal-card {
position: relative;
width: min(420px, 100%);
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
animation: rise 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes rise {
from { opacity: 0; transform: translateY(16px) scale(0.97); }
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.modal-head h2 { margin: 0; font-size: 17px; font-weight: 600; }
.icon-btn {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: 1px solid var(--line);
background: transparent;
border-radius: var(--r-sm);
color: var(--muted);
}
.icon-btn:hover { color: var(--text); border-color: var(--line-2); }
.icon-btn svg { width: 16px; height: 16px; }
.swap-box { position: relative; display: grid; gap: 8px; }
.swap-row {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
padding: 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.swap-k { font-size: 12px; color: var(--muted); grid-column: 1 / -1; margin-bottom: -2px; }
.swap-amt {
grid-column: 1 / 2;
border: 0;
background: transparent;
color: var(--text);
font-size: 24px;
font-weight: 600;
width: 100%;
min-width: 0;
}
.swap-amt:focus { outline: none; }
.swap-tok {
grid-column: 3 / 4;
font-size: 14px;
font-weight: 700;
padding: 8px 12px;
border-radius: var(--r-pill);
background: rgba(124, 92, 255, 0.16);
color: #cdbcff;
}
.swap-flip {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
display: grid;
place-items: center;
border: 3px solid var(--surface-2);
background: var(--elevated);
color: var(--accent-2);
border-radius: 12px;
z-index: 2;
transition: transform 0.2s, background 0.16s;
}
.swap-flip:hover { background: #2c303b; transform: translate(-50%, -50%) rotate(180deg); }
.swap-flip svg { width: 18px; height: 18px; }
.swap-info {
margin: 16px 0;
display: grid;
gap: 8px;
}
.swap-info div { display: flex; justify-content: space-between; font-size: 12.5px; }
.swap-info dt { color: var(--muted); margin: 0; }
.swap-info dd { margin: 0; }
.swap-impact { color: var(--pos); }
.risk {
display: flex;
gap: 9px;
align-items: flex-start;
padding: 11px 12px;
border: 1px solid rgba(255, 179, 71, 0.3);
background: rgba(255, 179, 71, 0.08);
border-radius: var(--r-md);
font-size: 12px;
color: #ffcf8f;
margin-bottom: 16px;
}
.risk svg { width: 16px; height: 16px; flex: none; margin-top: 1px; color: var(--warn); }
.confirm-btn {
position: relative;
width: 100%;
padding: 14px;
border: 0;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--accent), #6a4cff);
color: #fff;
font-size: 15px;
font-weight: 600;
box-shadow: 0 10px 28px var(--accent-glow);
transition: filter 0.16s, transform 0.12s;
}
.confirm-btn:hover { filter: brightness(1.08); }
.confirm-btn:active { transform: scale(0.99); }
.confirm-btn.is-loading { pointer-events: none; }
.confirm-loading { display: none; align-items: center; justify-content: center; gap: 9px; }
.confirm-btn.is-loading .confirm-default { display: none; }
.confirm-btn.is-loading .confirm-loading { display: flex; }
.spin {
width: 15px;
height: 15px;
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: grid;
gap: 8px;
z-index: 80;
width: min(360px, calc(100% - 28px));
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: var(--elevated);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
font-size: 13.5px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
animation: toastIn 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
.toast.out { animation: toastOut 0.3s ease forwards; }
.toast .t-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 8px var(--accent-2); flex: none; }
.toast .t-mono { font-family: "JetBrains Mono", monospace; color: var(--muted); margin-left: auto; font-size: 11.5px; }
@keyframes toastIn { from { opacity: 0; transform: translateY(14px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(14px); } }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.main { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.app { padding: 14px 12px 50px; }
.brand-name { font-size: 15px; }
.net-label { display: none; }
.net-btn { padding: 9px; }
.addr-text { font-size: 12px; }
.hero { padding: 18px 16px; }
.hero-actions { gap: 8px; }
.act { padding: 11px 4px; font-size: 11px; }
.act svg { width: 18px; height: 18px; }
.alloc { flex-direction: column; text-align: center; align-items: stretch; }
.alloc-donut { margin: 0 auto; }
.token-head { grid-template-columns: 1.6fr 1fr 1fr; }
.token-head .th-price { display: none; }
.token-row { grid-template-columns: 1.6fr 1fr 1fr; }
.tk-price { display: none; }
.tab { padding: 11px 12px; font-size: 13px; }
.nft-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}/* Lumen Wallet — UI-only simulation. No real wallet, RPC, or on-chain calls. */
(function () {
"use strict";
/* ---------------- Mock data ---------------- */
var ADDRESS = "0x7a3f9d2b8c4e1f0a6b5d3e2c9f87a14d3b21c41d";
var NETWORKS = {
lumen: { label: "Lumen Chain" },
nebula: { label: "Nebula" },
aurora: { label: "Aurora Testnet" },
solara: { label: "Solara" },
};
var TOKENS = [
{ sym: "NOVA", name: "Nova Protocol", color: "#7c5cff", price: 12.4, chg: 2.71, bal: 1820.42 },
{ sym: "USDL", name: "Lumen USD", color: "#00e0c6", price: 1.0, chg: 0.01, bal: 9640.0 },
{ sym: "ETHR", name: "Ether (wrapped)", color: "#8da2ff", price: 1842.6, chg: -1.34, bal: 4.812 },
{ sym: "PULSE", name: "Pulse", color: "#ffb347", price: 0.284, chg: 6.92, bal: 21450.0 },
{ sym: "GLOW", name: "Glow Finance", color: "#ff6ad5", price: 3.18, chg: -3.08, bal: 612.5 },
{ sym: "AQUA", name: "Aqua", color: "#46c8ff", price: 0.0412, chg: 0.42, bal: 88200.0 },
{ sym: "ORBIT", name: "Orbit DAO", color: "#9d6bff", price: 7.65, chg: 1.18, bal: 96.3 },
];
var NFTS = [
{ name: "Voidwalker #214", coll: "Lumen Genesis", price: 4.2, floor: 3.8, g: ["#7c5cff", "#00e0c6"], shape: "rings" },
{ name: "Prism Cat #07", coll: "Neon Felines", price: 1.05, floor: 0.92, g: ["#ff6ad5", "#ffb347"], shape: "tri" },
{ name: "Aurora Key", coll: "Solara Vaults", price: 12.0, floor: 9.4, g: ["#46c8ff", "#7c5cff"], shape: "grid" },
{ name: "Glitch Bloom", coll: "Static Garden", price: 0.74, floor: 0.6, g: ["#26d07c", "#00e0c6"], shape: "blob" },
{ name: "Mono Mask #88", coll: "Faceless", price: 2.3, floor: 2.1, g: ["#ffb347", "#ff4d6d"], shape: "tri" },
{ name: "Deep Signal", coll: "Lumen Genesis", price: 5.6, floor: 4.9, g: ["#9d6bff", "#ff6ad5"], shape: "rings" },
];
var ACTIVITY = [
{ type: "recv", title: "Received NOVA", from: "0x4d21…9af3", amt: "+120.00 NOVA", sub: "2m ago", status: "confirmed", pos: true },
{ type: "swap", title: "Swap ETHR → USDL", from: "Lumen DEX", amt: "+1,842.60 USDL", sub: "31m ago", status: "confirmed", pos: true },
{ type: "send", title: "Sent USDL", from: "0x91be…02cc", amt: "-450.00 USDL", sub: "1h ago", status: "pending" },
{ type: "mint", title: "Minted Voidwalker #214", from: "Lumen Genesis", amt: "-4.20 NOVA", sub: "4h ago", status: "confirmed" },
{ type: "send", title: "Sent GLOW", from: "0x7f08…12ba", amt: "-60.00 GLOW", sub: "Yesterday", status: "failed" },
{ type: "recv", title: "Received PULSE", from: "0x2a55…cd71", amt: "+9,000 PULSE", sub: "Yesterday", status: "confirmed", pos: true },
];
/* ---------------- Helpers ---------------- */
function $(s, r) { return (r || document).querySelector(s); }
function el(tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
}
function fmt(n, d) {
return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
}
function truncAddr(a) { return a.slice(0, 6) + "…" + a.slice(-4); }
var toastWrap = $("#toastWrap");
function toast(msg, mono) {
var t = el("div", "toast");
t.innerHTML = '<span class="t-dot"></span><span>' + msg + "</span>" +
(mono ? '<span class="t-mono">' + mono + "</span>" : "");
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("out");
setTimeout(function () { t.remove(); }, 320);
}, 2600);
}
/* ---------------- Animated number ---------------- */
function animateNum(node, target, decimals, prefix) {
var start = 0;
var dur = 1100;
var t0 = performance.now();
function step(now) {
var p = Math.min(1, (now - t0) / dur);
var e = 1 - Math.pow(1 - p, 3);
var v = start + (target - start) * e;
node.textContent = (prefix || "") + fmt(v, decimals);
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---------------- Render: hero ---------------- */
var totalValue = TOKENS.reduce(function (s, t) { return s + t.price * t.bal; }, 0);
var heroNum = $("#heroNum");
var heroDec = $("#heroDec");
var whole = Math.floor(totalValue);
var dec = Math.round((totalValue - whole) * 100);
heroDec.textContent = "." + String(dec).padStart(2, "0");
animateNum(heroNum, whole, 0);
/* ---------------- Render: allocation donut ---------------- */
var grouped = TOKENS.map(function (t) { return { sym: t.sym, color: t.color, val: t.price * t.bal }; })
.sort(function (a, b) { return b.val - a.val; });
var top = grouped.slice(0, 3);
var otherVal = grouped.slice(3).reduce(function (s, x) { return s + x.val; }, 0);
var segs = top.concat([{ sym: "Other", color: "#ff6ad5", val: otherVal }]);
var R = 48;
var CIRC = 2 * Math.PI * R;
var offset = 0;
var segNodes = document.querySelectorAll(".donut-seg");
var legend = $("#allocLegend");
segs.forEach(function (s, i) {
var pct = s.val / totalValue;
var len = pct * CIRC;
var node = segNodes[i];
// gap of 2px between segments
var gap = 2;
node.style.strokeDashoffset = -offset;
setTimeout(function () {
node.style.strokeDasharray = Math.max(0, len - gap) + " " + (CIRC - len + gap);
}, 80 + i * 90);
offset += len;
var li = el("li");
li.innerHTML =
'<span class="lg-dot" style="background:' + s.color + '"></span>' +
'<span class="lg-name">' + s.sym + "</span>" +
'<span class="lg-pct mono">' + (pct * 100).toFixed(1) + "%</span>";
legend.appendChild(li);
});
/* ---------------- Render: tokens ---------------- */
var tokenList = $("#tokenList");
TOKENS.slice().sort(function (a, b) { return b.price * b.bal - a.price * a.bal; }).forEach(function (t) {
var value = t.price * t.bal;
var up = t.chg >= 0;
var balDecimals = t.bal >= 1000 ? 2 : 4;
var li = el("li", "token-row");
li.tabIndex = 0;
li.setAttribute("role", "button");
li.innerHTML =
'<div class="tk-asset">' +
'<span class="tk-logo" style="background:' + t.color + '">' + t.sym.slice(0, 3) + "</span>" +
"<span><div class=\"tk-name\">" + t.sym + "</div><div class=\"tk-full\">" + t.name + "</div></span>" +
"</div>" +
'<div class="tk-price">' +
'<div class="tk-price-v mono">$' + fmt(t.price, t.price < 1 ? 4 : 2) + "</div>" +
'<div class="tk-chg ' + (up ? "up" : "down") + ' mono">' + (up ? "+" : "") + t.chg.toFixed(2) + "%</div>" +
"</div>" +
'<div class="tk-bal">' +
'<div class="mono">' + fmt(t.bal, balDecimals) + "</div>" +
'<div class="tk-bal-sub mono">' + t.sym + "</div>" +
"</div>" +
'<div class="tk-val mono">$' + fmt(value, 2) + "</div>";
li.addEventListener("click", function () { toast("Opening " + t.sym + " details", truncAddr(ADDRESS)); });
li.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); li.click(); } });
tokenList.appendChild(li);
});
/* ---------------- Render: NFTs ---------------- */
var nftGrid = $("#nftGrid");
function artInner(n) {
var g = "linear-gradient(135deg," + n.g[0] + "," + n.g[1] + ")";
var deco = "";
if (n.shape === "rings") {
deco = '<span style="position:absolute;inset:18%;border:2px solid rgba(255,255,255,.6);border-radius:50%"></span>' +
'<span style="position:absolute;inset:34%;border:2px solid rgba(255,255,255,.45);border-radius:50%"></span>' +
'<span style="position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px;background:#fff;border-radius:50%;opacity:.85"></span>';
} else if (n.shape === "tri") {
deco = '<span style="position:absolute;left:50%;top:54%;transform:translate(-50%,-50%);width:0;height:0;border-left:30px solid transparent;border-right:30px solid transparent;border-bottom:52px solid rgba(255,255,255,.78)"></span>';
} else if (n.shape === "grid") {
deco = '<span style="position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent 0 13px,rgba(255,255,255,.28) 13px 15px),repeating-linear-gradient(90deg,transparent 0 13px,rgba(255,255,255,.28) 13px 15px)"></span>';
} else {
deco = '<span style="position:absolute;inset:24%;background:rgba(255,255,255,.78);border-radius:46% 54% 38% 62%/52% 40% 60% 48%"></span>';
}
return '<div class="nft-art" style="background:' + g + '">' + deco + "</div>";
}
NFTS.forEach(function (n) {
var card = el("div", "nft-card");
card.tabIndex = 0;
card.setAttribute("role", "button");
card.innerHTML =
artInner(n) +
'<div class="nft-meta">' +
'<div class="nft-name">' + n.name + "</div>" +
'<div class="nft-coll">' + n.coll + "</div>" +
'<div class="nft-price"><b class="mono">' + n.price.toFixed(2) + "</b> NOVA" +
'<span class="nft-floor mono">floor ' + n.floor.toFixed(2) + "</span></div>" +
"</div>";
card.addEventListener("click", function () { toast("Viewing " + n.name, n.price.toFixed(2) + " NOVA"); });
card.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); card.click(); } });
nftGrid.appendChild(card);
});
/* ---------------- Render: activity ---------------- */
var ICONS = {
send: '<path d="M10 16V5m0 0L5.5 9.5M10 5l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
recv: '<path d="M10 4v11m0 0 4.5-4.5M10 15l-4.5-4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
swap: '<path d="M5 7h9m0 0-3-3m3 3-3 3M15 13H6m0 0 3 3m-3-3 3-3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
mint: '<path d="M10 5v10M5 10h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>',
};
var actList = $("#actList");
ACTIVITY.forEach(function (a) {
var li = el("li", "act-item");
li.innerHTML =
'<span class="act-ico ' + a.type + '"><svg viewBox="0 0 20 20" aria-hidden="true">' + ICONS[a.type] + "</svg></span>" +
'<div class="act-body">' +
'<div class="act-title">' + a.title + "</div>" +
'<div class="act-sub"><span class="mono">' + a.from + "</span><span>·</span><span>" + a.sub + "</span></div>" +
"</div>" +
"<div>" +
'<div class="act-amt ' + (a.pos ? "pos" : "neg") + ' mono">' + a.amt + "</div>" +
'<div style="text-align:right;margin-top:4px"><span class="act-status ' + a.status + '">' + a.status + "</span></div>" +
"</div>";
actList.appendChild(li);
});
/* ---------------- Tabs ---------------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
var ink = $("#tabInk");
function moveInk(tab) {
ink.style.left = tab.offsetLeft + "px";
ink.style.width = tab.offsetWidth + "px";
}
function selectTab(tab) {
tabs.forEach(function (t) {
var on = t === tab;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
});
document.querySelectorAll(".tabpanel").forEach(function (p) {
var on = p.getAttribute("data-panel") === tab.getAttribute("data-tab");
p.classList.toggle("is-active", on);
p.hidden = !on;
});
moveInk(tab);
}
tabs.forEach(function (t) {
t.addEventListener("click", function () { selectTab(t); });
});
// init ink after layout
requestAnimationFrame(function () { moveInk($(".tab.is-active")); });
window.addEventListener("resize", function () { moveInk($(".tab.is-active")); });
/* ---------------- Hide / show balances ---------------- */
var app = $("#app");
var eyeBtn = $("#eyeBtn");
eyeBtn.addEventListener("click", function () {
var hidden = app.classList.toggle("is-hidden");
eyeBtn.setAttribute("aria-pressed", hidden ? "true" : "false");
eyeBtn.setAttribute("aria-label", hidden ? "Show balances" : "Hide balances");
toast(hidden ? "Balances hidden" : "Balances visible");
});
/* ---------------- Address copy ---------------- */
$("#addrChip").addEventListener("click", function () {
var done = function () { toast("Address copied", truncAddr(ADDRESS)); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(ADDRESS).then(done, done);
} else { done(); }
});
/* ---------------- Network switcher ---------------- */
var netBtn = $("#netBtn");
var netMenu = $("#netMenu");
var netLabel = $("#netLabel");
var netDot = $("#netDot");
var heroChain = $("#heroChain");
function closeNet() {
netMenu.hidden = true;
netBtn.setAttribute("aria-expanded", "false");
}
netBtn.addEventListener("click", function (e) {
e.stopPropagation();
var open = netMenu.hidden;
netMenu.hidden = !open;
netBtn.setAttribute("aria-expanded", open ? "true" : "false");
});
netMenu.querySelectorAll("li").forEach(function (li) {
function pick() {
var key = li.getAttribute("data-net");
netMenu.querySelectorAll("li").forEach(function (x) { x.classList.remove("is-active"); });
li.classList.add("is-active");
netDot.setAttribute("data-net", key);
netLabel.textContent = NETWORKS[key].label;
heroChain.textContent = NETWORKS[key].label;
closeNet();
toast("Switched to " + NETWORKS[key].label);
}
li.addEventListener("click", pick);
li.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(); } });
});
document.addEventListener("click", function (e) {
if (!$("#netSwitch").contains(e.target)) closeNet();
});
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeNet(); });
/* ---------------- Action buttons ---------------- */
document.querySelectorAll(".act[data-act]").forEach(function (b) {
b.addEventListener("click", function () {
var a = b.getAttribute("data-act");
if (a === "Swap") { openSwap(); return; }
toast(a + " — opening flow", truncAddr(ADDRESS));
});
});
/* ---------------- Swap modal ---------------- */
var swapModal = $("#swapModal");
var payAmt = $("#payAmt");
var getAmt = $("#getAmt");
var swapRate = $("#swapRate");
var swapConfirm = $("#swapConfirm");
var RATE = 12.4;
var swapState = { from: "NOVA", to: "USDL" };
function recalc() {
var v = parseFloat(payAmt.value) || 0;
var out = swapState.from === "NOVA" ? v * RATE : v / RATE;
getAmt.value = fmt(out, 2);
var r = swapState.from === "NOVA"
? "1 NOVA = " + RATE.toFixed(2) + " USDL"
: "1 USDL = " + (1 / RATE).toFixed(4) + " NOVA";
swapRate.textContent = r;
}
function openSwap() {
swapModal.hidden = false;
recalc();
setTimeout(function () { payAmt.focus(); payAmt.select(); }, 60);
}
function closeSwap() { swapModal.hidden = true; }
swapModal.querySelectorAll("[data-close]").forEach(function (b) { b.addEventListener("click", closeSwap); });
document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !swapModal.hidden) closeSwap(); });
payAmt.addEventListener("input", recalc);
$("#swapFlip").addEventListener("click", function () {
var fromTok = $(".swap-row:first-child .swap-tok");
var toTok = $(".swap-row:last-child .swap-tok");
var tmp = swapState.from; swapState.from = swapState.to; swapState.to = tmp;
fromTok.textContent = swapState.from;
toTok.textContent = swapState.to;
recalc();
});
swapConfirm.addEventListener("click", function () {
if (swapConfirm.classList.contains("is-loading")) return;
swapConfirm.classList.add("is-loading");
setTimeout(function () {
swapConfirm.classList.remove("is-loading");
closeSwap();
var hash = "0x" + Math.random().toString(16).slice(2, 6) + "…" + Math.random().toString(16).slice(2, 6);
toast("Swap submitted (simulated)", hash);
}, 1700);
});
/* ---------------- Live price flicker (cosmetic) ---------------- */
setInterval(function () {
if (app.classList.contains("is-hidden")) return;
var drift = (Math.random() - 0.48) * 1.4;
var pct = Number($("#heroPnl .pnl-pct").textContent.replace(/[+%]/g, "")) + drift / 8;
// keep PnL realistic, do not animate hero number on every tick
}, 6000);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumen Wallet — Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app" id="app">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">Lumen<span class="brand-dim">Wallet</span></span>
</div>
<div class="topbar-right">
<!-- Network switcher -->
<div class="net-switch" id="netSwitch">
<button class="net-btn" id="netBtn" aria-haspopup="listbox" aria-expanded="false">
<span class="net-dot" id="netDot" data-net="lumen"></span>
<span class="net-label" id="netLabel">Lumen Chain</span>
<svg class="chev" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="net-menu" id="netMenu" role="listbox" aria-label="Select network" hidden>
<li role="option" tabindex="0" data-net="lumen" class="is-active"><span class="net-dot" data-net="lumen"></span>Lumen Chain<span class="net-meta mono">L1</span></li>
<li role="option" tabindex="0" data-net="nebula"><span class="net-dot" data-net="nebula"></span>Nebula<span class="net-meta mono">L2</span></li>
<li role="option" tabindex="0" data-net="aurora"><span class="net-dot" data-net="aurora"></span>Aurora Testnet<span class="net-meta mono">test</span></li>
<li role="option" tabindex="0" data-net="solara"><span class="net-dot" data-net="solara"></span>Solara<span class="net-meta mono">L1</span></li>
</ul>
</div>
<!-- Address chip -->
<button class="addr-chip" id="addrChip" title="Copy address">
<span class="addr-avatar" aria-hidden="true"></span>
<span class="addr-text mono" id="addrText">0x7a3f…c41d</span>
<svg class="copy-ico" viewBox="0 0 16 16" aria-hidden="true"><rect x="5.5" y="5.5" width="7.5" height="7.5" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M3.5 10.5V4a1 1 0 0 1 1-1H10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</button>
</div>
</header>
<main class="main">
<!-- Hero balance -->
<section class="hero" aria-label="Total balance">
<div class="hero-head">
<div class="hero-label">
Total balance
<button class="eye-btn" id="eyeBtn" aria-pressed="false" aria-label="Hide balances">
<svg class="eye-open" viewBox="0 0 20 20" aria-hidden="true"><path d="M1.5 10S4.5 4 10 4s8.5 6 8.5 6-3 6-8.5 6S1.5 10 1.5 10Z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="10" cy="10" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
<svg class="eye-off" viewBox="0 0 20 20" aria-hidden="true"><path d="M1.5 10S4.5 4 10 4c1.5 0 2.8.4 4 1M18.5 10S15.5 16 10 16c-1.5 0-2.8-.4-4-1" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3 3l14 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<span class="hero-chain mono" id="heroChain">Lumen Chain</span>
</div>
<div class="hero-value">
<span class="hero-cur">$</span><span class="hero-num mono" id="heroNum" data-value="48213.74">0.00</span>
<span class="hero-decimals mono" id="heroDec">.74</span>
</div>
<div class="hero-sub">
<span class="pnl pnl-pos" id="heroPnl">
<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M6 2.5 10 8H2z" fill="currentColor"/></svg>
<span class="mono" id="heroPnlVal">+$1,284.20</span>
<span class="mono pnl-pct">+2.74%</span>
</span>
<span class="hero-period">24h</span>
</div>
<div class="hero-actions">
<button class="act act-primary" data-act="Send">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 16V5m0 0L5.5 9.5M10 5l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
Send
</button>
<button class="act" data-act="Receive">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4v11m0 0 4.5-4.5M10 15l-4.5-4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
Receive
</button>
<button class="act" data-act="Swap" id="swapOpen">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 7h9m0 0-3-3m3 3-3 3M15 13H6m0 0 3 3m-3-3 3-3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
Swap
</button>
<button class="act" data-act="Buy">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 5v10M5 10h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
Buy
</button>
</div>
</section>
<!-- Allocation -->
<section class="alloc card" aria-label="Portfolio allocation">
<div class="alloc-donut">
<svg viewBox="0 0 120 120" class="donut" id="donut" aria-hidden="true">
<circle class="donut-track" cx="60" cy="60" r="48" />
<circle class="donut-seg" data-seg="0" cx="60" cy="60" r="48" />
<circle class="donut-seg" data-seg="1" cx="60" cy="60" r="48" />
<circle class="donut-seg" data-seg="2" cx="60" cy="60" r="48" />
<circle class="donut-seg" data-seg="3" cx="60" cy="60" r="48" />
</svg>
<div class="donut-center">
<span class="donut-k">Assets</span>
<span class="donut-v mono">7</span>
</div>
</div>
<ul class="alloc-legend" id="allocLegend"></ul>
</section>
<!-- Tabs -->
<section class="panel card">
<div class="tabs" role="tablist" aria-label="Wallet sections">
<button class="tab is-active" role="tab" aria-selected="true" id="tab-tokens" data-tab="tokens">Tokens</button>
<button class="tab" role="tab" aria-selected="false" id="tab-nfts" data-tab="nfts">NFTs <span class="tab-badge mono">6</span></button>
<button class="tab" role="tab" aria-selected="false" id="tab-activity" data-tab="activity">Activity</button>
<span class="tab-ink" id="tabInk"></span>
</div>
<!-- Tokens -->
<div class="tabpanel is-active" role="tabpanel" aria-labelledby="tab-tokens" data-panel="tokens">
<div class="token-head">
<span>Asset</span><span class="th-price">Price</span><span class="th-bal">Balance</span><span class="th-val">Value</span>
</div>
<ul class="token-list" id="tokenList"></ul>
</div>
<!-- NFTs -->
<div class="tabpanel" role="tabpanel" aria-labelledby="tab-nfts" data-panel="nfts" hidden>
<div class="nft-grid" id="nftGrid"></div>
</div>
<!-- Activity -->
<div class="tabpanel" role="tabpanel" aria-labelledby="tab-activity" data-panel="activity" hidden>
<ul class="act-list" id="actList"></ul>
</div>
</section>
</main>
<!-- Swap modal -->
<div class="modal" id="swapModal" hidden>
<div class="modal-scrim" data-close></div>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="swapTitle">
<div class="modal-head">
<h2 id="swapTitle">Swap tokens</h2>
<button class="icon-btn" data-close aria-label="Close">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</button>
</div>
<div class="swap-box">
<div class="swap-row">
<span class="swap-k">You pay</span>
<input class="swap-amt mono" id="payAmt" inputmode="decimal" value="1.50" aria-label="Amount to pay" />
<span class="swap-tok mono">NOVA</span>
</div>
<button class="swap-flip" id="swapFlip" aria-label="Flip direction">
<svg viewBox="0 0 18 18" aria-hidden="true"><path d="M6 3v9m0 0 3-3m-3 3-3-3M12 15V6m0 0 3 3m-3-3-3 3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="swap-row">
<span class="swap-k">You receive</span>
<input class="swap-amt mono" id="getAmt" readonly value="0.00" aria-label="Amount to receive" />
<span class="swap-tok mono">USDL</span>
</div>
</div>
<dl class="swap-info">
<div><dt>Rate</dt><dd class="mono" id="swapRate">1 NOVA = 12.40 USDL</dd></div>
<div><dt>Network fee</dt><dd class="mono">~0.0021 NOVA</dd></div>
<div><dt>Price impact</dt><dd class="mono swap-impact">0.18%</dd></div>
<div><dt>Slippage</dt><dd class="mono">0.5%</dd></div>
</dl>
<div class="risk">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.5 15 14H1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 6v3.2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="11.4" r="0.8" fill="currentColor"/></svg>
Review carefully. Signing approves a token transfer on a fictional chain — simulation only.
</div>
<button class="confirm-btn" id="swapConfirm">
<span class="confirm-default">Confirm swap</span>
<span class="confirm-loading"><span class="spin" aria-hidden="true"></span>Signing…</span>
</button>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Wallet Dashboard (balances · tokens · NFTs)
A full self-custody wallet dashboard rendered in the modern Web3 visual language: glassy translucent surfaces, neon gradient accents, soft glow on the primary action, and monospace formatting for every address, amount, and hash. The top bar pairs a copyable truncated address chip (0x7a3f…c41d) with a popover network switcher that hot-swaps between Lumen Chain, Nebula, Aurora Testnet, and Solara. The hero card uses a gradient border and a big animated fiat total that counts up on load, with a colored 24h PnL pill and Send / Receive / Swap / Buy buttons.
Below the hero, a CSS-drawn allocation donut animates its segments into place alongside a percentage legend. A tabbed panel switches between Tokens (rows with logo, price, 24h change, balance, and value), NFTs (a responsive grid of procedurally drawn thumbnails with floor prices), and Activity (a transaction feed with confirmed / pending / failed status chips). An animated underline slides between tabs.
Interactions are all vanilla JS: tab switching, the count-up balance, a hide-balances eye toggle that masks every sensitive value, address copy, and hover states throughout. The Swap action opens a guarded modal with a flip control, live rate recalculation, a price-impact / fee breakdown, an explicit risk warning, and a simulated signing state that resolves to a toast with a fake tx hash — making the confirm/sign step feel real without ever touching a chain.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.