Web3 — Liquidity Pool / Provide LP
A DeFi provide-liquidity component for an ETH/USDC pair with ratio-linked deposit inputs that auto-fill from the mock pool price, wallet balances with MAX buttons, a fee-tier selector (0.05/0.3/1%) that re-animates TVL, volume, and fee-APR stats, plus live pool-share and LP-token estimates. Includes Add and Remove tabs with a percent slider, a position summary card, and a sign-pending-success confirmation modal with an impermanent-loss warning and toast feedback.
MCP
Code
: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;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background:
radial-gradient(900px 480px at 12% -10%, rgba(124, 92, 255, 0.14), transparent 60%),
radial-gradient(720px 420px at 95% 0%, rgba(0, 224, 198, 0.08), transparent 55%),
var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
padding: 32px 20px 64px;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
}
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.muted { color: var(--muted); }
.lp-app {
max-width: 980px;
margin: 0 auto;
}
/* ===== Pool header ===== */
.pool-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.pair {
display: flex;
align-items: center;
gap: 14px;
}
.pair-icons {
display: flex;
}
.coin {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
font-size: 20px;
font-weight: 700;
border: 2px solid var(--bg);
flex: none;
}
.coin-eth {
background: linear-gradient(135deg, #6d7cff, #41338f);
color: #eef0ff;
box-shadow: 0 0 18px rgba(109, 124, 255, 0.35);
}
.coin-usdc {
background: linear-gradient(135deg, #1fb6d8, #0e5e92);
color: #e6fbff;
box-shadow: 0 0 18px rgba(31, 182, 216, 0.3);
}
.pair-icons .coin-usdc {
margin-left: -12px;
}
.coin.sm {
width: 22px;
height: 22px;
font-size: 11px;
border-width: 1px;
}
.coin.sm.inline {
vertical-align: -5px;
margin-right: 2px;
}
.pair-meta h1 {
margin: 0;
font-size: 1.4rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.pair-sub {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 2px;
}
.chain-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
font-weight: 600;
color: var(--accent-2);
background: rgba(0, 224, 198, 0.1);
border: 1px solid rgba(0, 224, 198, 0.28);
border-radius: var(--r-pill);
padding: 2px 10px;
}
.chain-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 8px var(--accent-2);
}
.fee-pill {
font-size: 0.75rem;
color: var(--accent);
background: rgba(124, 92, 255, 0.12);
border: 1px solid rgba(124, 92, 255, 0.35);
border-radius: var(--r-pill);
padding: 2px 10px;
font-weight: 500;
}
.addr {
font-size: 0.75rem;
color: var(--muted);
}
.wallet-chip {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--line);
border-radius: var(--r-pill);
padding: 8px 16px;
font-size: 0.85rem;
backdrop-filter: blur(8px);
}
.wallet-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px var(--pos);
}
/* ===== Stats strip ===== */
.stats-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.stat {
background: rgba(19, 21, 28, 0.75);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 2px;
backdrop-filter: blur(10px);
transition: border-color 0.2s ease, transform 0.2s ease;
}
.stat:hover {
border-color: var(--line-2);
transform: translateY(-1px);
}
.stat-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.stat-value {
font-size: 1.15rem;
font-weight: 700;
}
.stat-delta {
font-size: 0.72rem;
}
.stat-delta.muted { color: var(--muted); }
/* ===== Layout grid ===== */
.lp-grid {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.panel {
border-radius: var(--r-lg);
padding: 22px;
}
.glass {
background: linear-gradient(180deg, rgba(27, 30, 39, 0.85), rgba(19, 21, 28, 0.9));
border: 1px solid var(--line);
backdrop-filter: blur(14px);
position: relative;
}
.panel.glass::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.5), transparent 35%, transparent 65%, rgba(0, 224, 198, 0.4));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ===== Tabs ===== */
.tabs {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
background: rgba(10, 11, 15, 0.6);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px;
margin-bottom: 20px;
}
.tab {
position: relative;
z-index: 1;
appearance: none;
background: none;
border: 0;
color: var(--muted);
font: inherit;
font-weight: 600;
padding: 9px 0;
border-radius: 10px;
cursor: pointer;
transition: color 0.2s ease;
}
.tab.is-active {
color: var(--text);
}
.tab:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.tab-glider {
position: absolute;
top: 4px;
left: 4px;
width: calc(50% - 4px);
height: calc(100% - 8px);
border-radius: 10px;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.35), rgba(124, 92, 255, 0.15));
border: 1px solid rgba(124, 92, 255, 0.45);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.tabs.remove-active .tab-glider {
transform: translateX(100%);
background: linear-gradient(135deg, rgba(255, 77, 109, 0.3), rgba(255, 77, 109, 0.12));
border-color: rgba(255, 77, 109, 0.45);
}
/* ===== Fee tiers ===== */
.fee-tiers {
border: 0;
padding: 0;
margin: 0 0 16px;
}
.fee-tiers legend {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
padding: 0;
margin-bottom: 8px;
}
.fee-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.fee-opt input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.fee-card {
display: flex;
flex-direction: column;
gap: 2px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
.fee-card:hover {
border-color: var(--line-2);
transform: translateY(-1px);
}
.fee-opt input:checked + .fee-card {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 6px 22px -8px var(--accent-glow);
background: linear-gradient(180deg, rgba(124, 92, 255, 0.12), var(--surface-2));
}
.fee-opt input:focus-visible + .fee-card {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.fee-rate {
font-weight: 700;
font-size: 0.95rem;
}
.fee-desc {
font-size: 0.72rem;
color: var(--muted);
}
.fee-share {
font-size: 0.68rem;
color: var(--accent-2);
margin-top: 4px;
}
/* ===== Token inputs ===== */
.token-box {
background: rgba(10, 11, 15, 0.6);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.token-box:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent-glow);
}
.token-box-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.token-box-top label {
font-size: 0.78rem;
color: var(--muted);
}
.balance {
font-size: 0.74rem;
color: var(--muted);
display: inline-flex;
align-items: center;
gap: 6px;
}
.max-btn {
appearance: none;
font-family: "JetBrains Mono", monospace;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--accent-2);
background: rgba(0, 224, 198, 0.1);
border: 1px solid rgba(0, 224, 198, 0.3);
border-radius: var(--r-sm);
padding: 2px 7px;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
}
.max-btn:hover {
background: rgba(0, 224, 198, 0.2);
}
.max-btn:active {
transform: scale(0.95);
}
.token-box-main {
display: flex;
align-items: center;
gap: 12px;
}
.amount {
flex: 1;
min-width: 0;
appearance: none;
background: none;
border: 0;
color: var(--text);
font-size: 1.5rem;
font-weight: 500;
padding: 0;
}
.amount::placeholder {
color: var(--muted);
opacity: 0.6;
}
.amount:focus {
outline: none;
}
.token-tag {
display: inline-flex;
align-items: center;
gap: 7px;
background: var(--elevated);
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
padding: 5px 12px 5px 6px;
font-weight: 700;
font-size: 0.9rem;
flex: none;
}
.token-box-sub {
font-size: 0.74rem;
color: var(--muted);
margin-top: 6px;
}
.link-divider {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 8px 0;
color: var(--muted);
}
.link-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--accent);
font-size: 0.95rem;
}
.link-note {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* ===== Estimates ===== */
.estimates {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 16px;
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.est-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 0.84rem;
}
.est-row > span:first-child {
color: var(--muted);
}
.est-sub {
border-top: 1px dashed var(--line);
padding-top: 8px;
font-size: 0.76rem;
}
.apr-badge {
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warn);
background: rgba(255, 179, 71, 0.12);
border: 1px solid rgba(255, 179, 71, 0.3);
border-radius: var(--r-pill);
padding: 1px 7px;
margin-left: 4px;
}
/* ===== CTA ===== */
.cta {
appearance: none;
width: 100%;
margin-top: 16px;
border: 0;
border-radius: var(--r-md);
padding: 15px;
font: inherit;
font-weight: 700;
font-size: 1rem;
color: #fff;
background: linear-gradient(135deg, var(--accent), #5a3de0);
box-shadow: 0 8px 28px -8px var(--accent-glow);
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
.cta:hover:not(:disabled) {
filter: brightness(1.1);
box-shadow: 0 10px 34px -8px var(--accent-glow);
}
.cta:active:not(:disabled) {
transform: scale(0.985);
}
.cta:disabled {
background: var(--surface-2);
color: var(--muted);
box-shadow: none;
cursor: not-allowed;
}
.cta-danger {
background: linear-gradient(135deg, var(--neg), #d12c4e);
box-shadow: 0 8px 28px -10px rgba(255, 77, 109, 0.5);
}
.cta-danger:hover:not(:disabled) {
box-shadow: 0 10px 34px -10px rgba(255, 77, 109, 0.55);
}
.risk-note {
font-size: 0.74rem;
color: var(--muted);
margin: 12px 0 0;
text-align: center;
}
.risk-note strong {
color: var(--warn);
}
/* ===== Remove pane ===== */
.remove-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 6px;
}
.remove-label {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.remove-pct {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.pct-slider {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--accent) var(--p, 25%), var(--surface-2) var(--p, 25%));
border: 1px solid var(--line);
cursor: pointer;
margin: 10px 0 14px;
}
.pct-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--accent);
box-shadow: 0 0 14px var(--accent-glow);
cursor: grab;
}
.pct-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--accent);
box-shadow: 0 0 14px var(--accent-glow);
cursor: grab;
}
.pct-presets {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.pct-presets button {
appearance: none;
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
font-weight: 500;
color: var(--muted);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 7px 0;
cursor: pointer;
transition: all 0.15s ease;
}
.pct-presets button:hover {
color: var(--text);
border-color: var(--line-2);
}
.pct-presets button.is-active {
color: var(--accent);
border-color: var(--accent);
background: rgba(124, 92, 255, 0.12);
}
/* ===== Position panel ===== */
.pos-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.pos-head h2 {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.72rem;
font-weight: 600;
color: var(--pos);
background: rgba(38, 208, 124, 0.1);
border: 1px solid rgba(38, 208, 124, 0.3);
border-radius: var(--r-pill);
padding: 3px 10px;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px var(--pos);
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.pos-value {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 16px;
}
.pos-value-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.pos-value-num {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.pos-delta {
font-size: 0.78rem;
}
.pos-tokens {
list-style: none;
margin: 0 0 14px;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.pos-tokens li {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(10, 11, 15, 0.55);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 11px 14px;
}
.pos-token {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 0.9rem;
}
.pos-amts {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 0.9rem;
}
.pos-amts .sub {
font-size: 0.72rem;
color: var(--muted);
}
.pos-meta {
border-top: 1px solid var(--line);
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 7px;
margin-bottom: 16px;
}
.pos-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.8rem;
}
.pos-meta-row > span:first-child {
color: var(--muted);
}
.apr-card {
background: linear-gradient(135deg, rgba(124, 92, 255, 0.14), rgba(0, 224, 198, 0.08));
border: 1px solid rgba(124, 92, 255, 0.3);
border-radius: var(--r-md);
padding: 14px 16px;
}
.apr-card-top {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.82rem;
color: var(--muted);
margin-bottom: 8px;
}
.apr-num {
font-size: 1.3rem;
font-weight: 700;
color: var(--accent-2);
text-shadow: 0 0 18px rgba(0, 224, 198, 0.4);
}
.apr-bar {
height: 6px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.07);
overflow: hidden;
margin-bottom: 8px;
}
.apr-bar-fill {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.apr-foot {
font-size: 0.68rem;
color: var(--muted);
}
/* ===== Modal ===== */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(5, 6, 10, 0.7);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 50;
animation: fadeIn 0.2s ease;
}
.modal-backdrop[hidden] {
display: none;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
width: min(440px, 100%);
border-radius: var(--r-lg);
padding: 24px;
animation: popIn 0.25s cubic-bezier(0.34, 1.4, 0.64, 1);
}
@keyframes popIn {
from { opacity: 0; transform: scale(0.94) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal h3 {
margin: 0 0 16px;
font-size: 1.15rem;
}
.modal-step[hidden] {
display: none;
}
#modal-pending,
#modal-success {
text-align: center;
}
#modal-pending h3,
#modal-success h3 {
margin: 14px 0 4px;
}
.confirm-pair {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
background: rgba(10, 11, 15, 0.6);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 14px;
flex-wrap: wrap;
}
.confirm-token {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.confirm-amt {
font-size: 1.05rem;
font-weight: 700;
}
.confirm-sym {
font-size: 0.72rem;
color: var(--muted);
letter-spacing: 0.06em;
}
.confirm-plus {
font-size: 1.3rem;
color: var(--muted);
}
.modal-est {
margin-top: 14px;
}
.warn-box {
display: flex;
gap: 10px;
align-items: flex-start;
background: rgba(255, 179, 71, 0.08);
border: 1px solid rgba(255, 179, 71, 0.3);
border-radius: var(--r-md);
padding: 12px 14px;
font-size: 0.78rem;
color: var(--text);
margin-top: 14px;
}
.warn-icon {
color: var(--warn);
font-size: 1rem;
line-height: 1.2;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 4px;
}
.modal-actions .cta {
margin-top: 16px;
}
.ghost-btn {
appearance: none;
flex: 0 0 32%;
margin-top: 16px;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: none;
color: var(--text);
font: inherit;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
}
.ghost-btn:hover {
background: rgba(255, 255, 255, 0.06);
}
.spinner {
width: 52px;
height: 52px;
margin: 6px auto 0;
border-radius: 50%;
border: 3px solid var(--line-2);
border-top-color: var(--accent);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.pending-sub {
margin: 0 0 10px;
color: var(--muted);
font-size: 0.84rem;
}
.tx-hash {
display: inline-block;
font-size: 0.74rem;
color: var(--accent-2);
background: rgba(0, 224, 198, 0.08);
border: 1px solid rgba(0, 224, 198, 0.25);
border-radius: var(--r-pill);
padding: 4px 12px;
}
.success-ring {
width: 56px;
height: 56px;
margin: 6px auto 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: var(--pos);
border: 2px solid var(--pos);
box-shadow: 0 0 26px rgba(38, 208, 124, 0.4);
animation: popIn 0.4s cubic-bezier(0.34, 1.6, 0.64, 1);
}
/* ===== Toasts ===== */
.toast-zone {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 60;
width: min(420px, calc(100vw - 32px));
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--elevated);
border: 1px solid var(--line-2);
border-left: 3px solid var(--accent);
border-radius: var(--r-md);
padding: 12px 16px;
font-size: 0.84rem;
box-shadow: 0 12px 40px -12px rgba(0, 0, 0, 0.7);
animation: toastIn 0.3s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.toast.ok { border-left-color: var(--pos); }
.toast.warn { border-left-color: var(--warn); }
.toast.is-leaving {
animation: toastOut 0.25s ease forwards;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(10px); }
}
/* Value flash on animated numbers */
.flash {
animation: flash 0.5s ease;
}
@keyframes flash {
0% { color: var(--accent-2); }
100% { color: inherit; }
}
/* ===== Responsive ===== */
@media (max-width: 880px) {
.lp-grid {
grid-template-columns: 1fr;
}
.stats-strip {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
body {
padding: 18px 12px 48px;
}
.panel {
padding: 16px;
}
.fee-options {
grid-template-columns: 1fr;
}
.fee-card {
flex-direction: row;
align-items: center;
gap: 10px;
}
.fee-share {
margin-top: 0;
margin-left: auto;
}
.amount {
font-size: 1.25rem;
}
.pos-value-num {
font-size: 1.6rem;
}
.remove-pct {
font-size: 1.6rem;
}
.wallet-chip {
padding: 6px 12px;
font-size: 0.78rem;
}
.pair-meta h1 {
font-size: 1.15rem;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}/* Web3 — Liquidity Pool / Provide LP (UI simulation, mock data only) */
(() => {
"use strict";
// ---------- Mock pool data ----------
const ETH_PRICE = 2584.3; // USDC per ETH (mock pool price)
const TIERS = {
"0.0005": { label: "0.05%", tvl: 3_860_000, vol24h: 4_200_000, lpSupply: 31_000 },
"0.003": { label: "0.30%", tvl: 48_200_000, vol24h: 9_700_000, lpSupply: 377_200 },
"0.01": { label: "1.00%", tvl: 2_760_000, vol24h: 410_000, lpSupply: 22_400 },
};
const wallet = { eth: 4.2618, usdc: 12_840.55 };
const position = { eth: 1.672, usdc: 4_320.97, lp: 42.25, feesLifetime: 127.66, unclaimed: 31.92 };
let currentFee = "0.003";
let removePct = 25;
let modalMode = "add"; // "add" | "remove"
let pendingTimer = null;
// ---------- DOM ----------
const $ = (id) => document.getElementById(id);
const els = {
feePill: $("head-fee-pill"),
statTvl: $("stat-tvl"),
statVol: $("stat-vol"),
statFees: $("stat-fees"),
statPrice: $("stat-price"),
tabs: document.querySelector(".tabs"),
tabAdd: $("tab-add"),
tabRemove: $("tab-remove"),
paneAdd: $("pane-add"),
paneRemove: $("pane-remove"),
amtEth: $("amount-eth"),
amtUsdc: $("amount-usdc"),
usdEth: $("usd-eth"),
usdUsdc: $("usd-usdc"),
balEth: $("bal-eth"),
balUsdc: $("bal-usdc"),
estShare: $("est-share"),
estLp: $("est-lp"),
estApr: $("est-apr"),
btnAdd: $("btn-add"),
removeSlider: $("remove-slider"),
removePctEl: $("remove-pct"),
rmEth: $("rm-eth"),
rmUsdc: $("rm-usdc"),
rmLp: $("rm-lp"),
btnRemove: $("btn-remove"),
posValue: $("pos-value"),
posEth: $("pos-eth"),
posEthUsd: $("pos-eth-usd"),
posUsdc: $("pos-usdc"),
posUsdcUsd: $("pos-usdc-usd"),
posLp: $("pos-lp"),
posShare: $("pos-share"),
posFees: $("pos-fees"),
posApr: $("pos-apr"),
aprBarFill: document.querySelector(".apr-bar-fill"),
modal: $("modal"),
stepConfirm: $("modal-confirm"),
stepPending: $("modal-pending"),
stepSuccess: $("modal-success"),
modalTitle: $("modal-title"),
cfEth: $("cf-eth"),
cfUsdc: $("cf-usdc"),
cfFee: $("cf-fee"),
cfShare: $("cf-share"),
cfLp: $("cf-lp"),
btnCancel: $("btn-cancel"),
btnConfirm: $("btn-confirm"),
btnDone: $("btn-done"),
pendingTitle: document.querySelector("#modal-pending h3"),
pendingHash: $("tx-hash"),
successTitle: document.querySelector("#modal-success h3"),
successHash: document.querySelector("#modal-success .tx-hash"),
toastZone: $("toast-zone"),
};
// ---------- Helpers ----------
const fmt = (n, dp = 4) =>
n.toLocaleString("en-US", { minimumFractionDigits: dp, maximumFractionDigits: dp });
const fmtUsd = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmtCompact = (n) => {
if (n >= 1e6) return "$" + (n / 1e6).toFixed(1) + "M";
if (n >= 1e3) return "$" + (n / 1e3).toFixed(1) + "K";
return fmtUsd(n);
};
const parseAmt = (str) => {
const n = parseFloat(String(str).replace(/,/g, ""));
return Number.isFinite(n) && n > 0 ? n : 0;
};
const sanitize = (input) => {
let v = input.value.replace(/[^0-9.]/g, "");
const firstDot = v.indexOf(".");
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, "");
}
if (v !== input.value) input.value = v;
return v;
};
const feeApr = (key) => {
const t = TIERS[key];
return ((t.vol24h * parseFloat(key) * 365) / t.tvl) * 100;
};
const randHash = () => {
const hex = "0123456789abcdef";
const part = (len) =>
Array.from({ length: len }, () => hex[(Math.random() * 16) | 0]).join("");
return `0x${part(4)}…${part(4)}`;
};
// Count-up animation for stat values
const animateValue = (el, to, formatter, dur = 600) => {
const from = parseFloat(el.dataset.raw ?? to) || 0;
el.dataset.raw = String(to);
const start = performance.now();
const ease = (t) => 1 - Math.pow(1 - t, 3);
const tick = (now) => {
const p = Math.min(1, (now - start) / dur);
el.textContent = formatter(from + (to - from) * ease(p));
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
el.classList.remove("flash");
void el.offsetWidth;
el.classList.add("flash");
};
// ---------- Toast ----------
function toast(msg, type = "ok") {
const el = document.createElement("div");
el.className = `toast ${type}`;
el.role = "status";
el.textContent = msg;
els.toastZone.appendChild(el);
setTimeout(() => {
el.classList.add("is-leaving");
el.addEventListener("animationend", () => el.remove(), { once: true });
}, 3400);
}
// ---------- Renderers ----------
function renderBalances() {
els.balEth.textContent = fmt(wallet.eth, 4);
els.balUsdc.textContent = fmt(wallet.usdc, 2);
}
function renderPoolStats(animated = true) {
const t = TIERS[currentFee];
const fees = t.vol24h * parseFloat(currentFee);
if (animated) {
animateValue(els.statTvl, t.tvl, fmtCompact);
animateValue(els.statVol, t.vol24h, fmtCompact);
animateValue(els.statFees, fees, fmtCompact);
} else {
els.statTvl.textContent = fmtCompact(t.tvl);
els.statVol.textContent = fmtCompact(t.vol24h);
els.statFees.textContent = fmtCompact(fees);
els.statTvl.dataset.raw = String(t.tvl);
els.statVol.dataset.raw = String(t.vol24h);
els.statFees.dataset.raw = String(fees);
}
els.statPrice.textContent = `${fmt(ETH_PRICE, 2)} USDC`;
els.feePill.textContent = `${TIERS[currentFee].label} fee`;
const apr = feeApr(currentFee);
els.estApr.textContent = apr.toFixed(1) + "%";
els.posApr.textContent = apr.toFixed(1) + "%";
els.aprBarFill.style.width = Math.min(100, (apr / 35) * 100).toFixed(0) + "%";
}
function renderPosition() {
const ethUsd = position.eth * ETH_PRICE;
const total = ethUsd + position.usdc;
els.posValue.textContent = fmtUsd(total);
els.posEth.textContent = fmt(position.eth, 4);
els.posEthUsd.textContent = fmtUsd(ethUsd);
els.posUsdc.textContent = fmt(position.usdc, 2);
els.posUsdcUsd.textContent = fmtUsd(position.usdc);
els.posLp.textContent = `${fmt(position.lp, 4)} LUM-LP`;
els.posShare.textContent =
((position.lp / TIERS["0.003"].lpSupply) * 100).toFixed(4) + "%";
els.posFees.textContent = fmtUsd(position.feesLifetime);
}
function renderEstimates() {
const eth = parseAmt(els.amtEth.value);
const usdc = parseAmt(els.amtUsdc.value);
const t = TIERS[currentFee];
els.usdEth.textContent = "≈ " + fmtUsd(eth * ETH_PRICE);
els.usdUsdc.textContent = "≈ " + fmtUsd(usdc);
const depositUsd = eth * ETH_PRICE + usdc;
const share = depositUsd > 0 ? (depositUsd / (t.tvl + depositUsd)) * 100 : 0;
const lpMinted = (depositUsd / t.tvl) * t.lpSupply;
els.estShare.textContent = share.toFixed(4) + "%";
els.estLp.textContent = `${fmt(lpMinted, 4)} LUM-LP`;
// CTA state
if (depositUsd <= 0) {
els.btnAdd.disabled = true;
els.btnAdd.textContent = "Enter an amount";
} else if (eth > wallet.eth) {
els.btnAdd.disabled = true;
els.btnAdd.textContent = "Insufficient ETH balance";
} else if (usdc > wallet.usdc) {
els.btnAdd.disabled = true;
els.btnAdd.textContent = "Insufficient USDC balance";
} else {
els.btnAdd.disabled = false;
els.btnAdd.textContent = "Add liquidity";
}
}
function renderRemove() {
const pct = removePct;
els.removePctEl.textContent = pct + "%";
els.removeSlider.value = String(pct);
els.removeSlider.style.setProperty("--p", pct + "%");
document.querySelectorAll(".pct-presets button").forEach((b) => {
b.classList.toggle("is-active", Number(b.dataset.pct) === pct);
});
els.rmEth.textContent = fmt(position.eth * (pct / 100), 4);
els.rmUsdc.textContent = fmt(position.usdc * (pct / 100), 2);
els.rmLp.textContent = `${fmt(position.lp * (pct / 100), 4)} LUM-LP`;
els.btnRemove.disabled = pct === 0 || position.lp <= 0;
els.btnRemove.textContent =
position.lp <= 0 ? "No position to remove" : `Remove ${pct}% liquidity`;
}
// ---------- Tabs ----------
function setTab(which) {
const isAdd = which === "add";
els.tabAdd.classList.toggle("is-active", isAdd);
els.tabRemove.classList.toggle("is-active", !isAdd);
els.tabAdd.setAttribute("aria-selected", String(isAdd));
els.tabRemove.setAttribute("aria-selected", String(!isAdd));
els.paneAdd.hidden = !isAdd;
els.paneRemove.hidden = isAdd;
els.tabs.classList.toggle("remove-active", !isAdd);
}
els.tabAdd.addEventListener("click", () => setTab("add"));
els.tabRemove.addEventListener("click", () => setTab("remove"));
// ---------- Ratio-linked inputs ----------
els.amtEth.addEventListener("input", () => {
const v = sanitize(els.amtEth);
const eth = parseAmt(v);
els.amtUsdc.value = eth > 0 ? (eth * ETH_PRICE).toFixed(2) : "";
renderEstimates();
});
els.amtUsdc.addEventListener("input", () => {
const v = sanitize(els.amtUsdc);
const usdc = parseAmt(v);
els.amtEth.value = usdc > 0 ? (usdc / ETH_PRICE).toFixed(6) : "";
renderEstimates();
});
document.querySelectorAll(".max-btn").forEach((btn) => {
btn.addEventListener("click", () => {
if (btn.dataset.max === "eth") {
els.amtEth.value = String(wallet.eth);
els.amtEth.dispatchEvent(new Event("input"));
} else {
els.amtUsdc.value = wallet.usdc.toFixed(2);
els.amtUsdc.dispatchEvent(new Event("input"));
}
});
});
// ---------- Fee tier ----------
document.querySelectorAll('input[name="fee"]').forEach((radio) => {
radio.addEventListener("change", () => {
currentFee = radio.value;
renderPoolStats(true);
renderEstimates();
toast(`Switched to the ${TIERS[currentFee].label} fee tier`, "ok");
});
});
// ---------- Remove slider + presets ----------
els.removeSlider.addEventListener("input", () => {
removePct = Number(els.removeSlider.value);
renderRemove();
});
document.querySelectorAll(".pct-presets button").forEach((btn) => {
btn.addEventListener("click", () => {
removePct = Number(btn.dataset.pct);
renderRemove();
});
});
// ---------- Modal flow ----------
function showStep(step) {
els.stepConfirm.hidden = step !== "confirm";
els.stepPending.hidden = step !== "pending";
els.stepSuccess.hidden = step !== "success";
}
function openModal(mode) {
modalMode = mode;
const t = TIERS[currentFee];
if (mode === "add") {
const eth = parseAmt(els.amtEth.value);
const usdc = parseAmt(els.amtUsdc.value);
const depositUsd = eth * ETH_PRICE + usdc;
els.modalTitle.textContent = "Confirm add liquidity";
els.cfEth.textContent = fmt(eth, 4);
els.cfUsdc.textContent = fmt(usdc, 2);
els.cfShare.textContent = ((depositUsd / (t.tvl + depositUsd)) * 100).toFixed(4) + "%";
els.cfLp.textContent = `${fmt((depositUsd / t.tvl) * t.lpSupply, 4)} LUM-LP`;
els.btnConfirm.textContent = "Sign & supply";
els.pendingTitle.textContent = "Supplying liquidity…";
els.successTitle.textContent = "Liquidity added";
} else {
els.modalTitle.textContent = "Confirm remove liquidity";
els.cfEth.textContent = fmt(position.eth * (removePct / 100), 4);
els.cfUsdc.textContent = fmt(position.usdc * (removePct / 100), 2);
els.cfShare.textContent = removePct + "% of position";
els.cfLp.textContent = `${fmt(position.lp * (removePct / 100), 4)} LUM-LP burned`;
els.btnConfirm.textContent = "Sign & remove";
els.pendingTitle.textContent = "Removing liquidity…";
els.successTitle.textContent = "Liquidity removed";
}
els.cfFee.textContent = TIERS[currentFee].label;
showStep("confirm");
els.modal.hidden = false;
els.btnConfirm.focus();
}
function closeModal() {
if (pendingTimer) {
clearTimeout(pendingTimer);
pendingTimer = null;
}
els.modal.hidden = true;
}
els.btnAdd.addEventListener("click", () => openModal("add"));
els.btnRemove.addEventListener("click", () => openModal("remove"));
els.btnCancel.addEventListener("click", closeModal);
els.modal.addEventListener("click", (e) => {
if (e.target === els.modal) closeModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !els.modal.hidden) closeModal();
});
els.btnConfirm.addEventListener("click", () => {
const hash = randHash();
const block = (18_442_900 + ((Math.random() * 40) | 0)).toLocaleString("en-US");
els.pendingHash.textContent = hash;
els.successHash.textContent = `${hash} · block ${block}`;
showStep("pending");
pendingTimer = setTimeout(() => {
pendingTimer = null;
showStep("success");
}, 1600);
});
els.btnDone.addEventListener("click", () => {
const t = TIERS[currentFee];
if (modalMode === "add") {
const eth = parseAmt(els.amtEth.value);
const usdc = parseAmt(els.amtUsdc.value);
const depositUsd = eth * ETH_PRICE + usdc;
wallet.eth = Math.max(0, wallet.eth - eth);
wallet.usdc = Math.max(0, wallet.usdc - usdc);
position.eth += eth;
position.usdc += usdc;
position.lp += (depositUsd / t.tvl) * t.lpSupply;
els.amtEth.value = "";
els.amtUsdc.value = "";
toast(`Added ${fmt(eth, 4)} ETH + ${fmt(usdc, 2)} USDC to the pool`, "ok");
} else {
const f = removePct / 100;
const ethOut = position.eth * f;
const usdcOut = position.usdc * f;
wallet.eth += ethOut;
wallet.usdc += usdcOut + position.unclaimed;
position.feesLifetime += position.unclaimed;
position.unclaimed = 0;
position.eth -= ethOut;
position.usdc -= usdcOut;
position.lp -= position.lp * f;
toast(`Removed ${removePct}% — received ${fmt(ethOut, 4)} ETH + ${fmt(usdcOut, 2)} USDC`, "ok");
}
closeModal();
renderBalances();
renderPosition();
renderEstimates();
renderRemove();
});
// ---------- Init ----------
renderBalances();
renderPoolStats(false);
renderPosition();
renderEstimates();
renderRemove();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web3 — Liquidity Pool / Provide LP</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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="lp-app" aria-label="Liquidity pool — provide LP">
<!-- ===== Pool header ===== -->
<header class="pool-head">
<div class="pair">
<div class="pair-icons" aria-hidden="true">
<span class="coin coin-eth">Ξ</span>
<span class="coin coin-usdc">$</span>
</div>
<div class="pair-meta">
<h1>ETH / USDC</h1>
<div class="pair-sub">
<span class="chain-pill"><span class="chain-dot"></span>Lumen Chain</span>
<span class="fee-pill mono" id="head-fee-pill">0.30% fee</span>
<span class="addr mono" title="Pool contract (fictional)">0x7a3f…c41d</span>
</div>
</div>
</div>
<div class="wallet-chip" role="status" aria-label="Connected wallet (simulated)">
<span class="wallet-dot" aria-hidden="true"></span>
<span class="mono">0x4Be2…9aF1</span>
</div>
</header>
<!-- ===== Pool stats strip ===== -->
<section class="stats-strip" aria-label="Pool statistics">
<div class="stat">
<span class="stat-label">TVL</span>
<span class="stat-value mono" id="stat-tvl" data-animate>$48.2M</span>
<span class="stat-delta pos mono">+2.4%</span>
</div>
<div class="stat">
<span class="stat-label">24h Volume</span>
<span class="stat-value mono" id="stat-vol" data-animate>$9.7M</span>
<span class="stat-delta neg mono">−1.1%</span>
</div>
<div class="stat">
<span class="stat-label">24h Fees</span>
<span class="stat-value mono" id="stat-fees">$29.1K</span>
<span class="stat-delta pos mono">+0.8%</span>
</div>
<div class="stat">
<span class="stat-label">Pool price</span>
<span class="stat-value mono" id="stat-price">2,584.30 USDC</span>
<span class="stat-delta muted mono">per ETH</span>
</div>
</section>
<div class="lp-grid">
<!-- ===== Left: Add / Remove panel ===== -->
<section class="panel glass" aria-label="Manage liquidity">
<div class="tabs" role="tablist" aria-label="Add or remove liquidity">
<button class="tab is-active" id="tab-add" role="tab" aria-selected="true" aria-controls="pane-add">Add</button>
<button class="tab" id="tab-remove" role="tab" aria-selected="false" aria-controls="pane-remove">Remove</button>
<span class="tab-glider" aria-hidden="true"></span>
</div>
<!-- ---- ADD pane ---- -->
<div class="pane" id="pane-add" role="tabpanel" aria-labelledby="tab-add">
<!-- Fee tier selector -->
<fieldset class="fee-tiers">
<legend>Fee tier</legend>
<div class="fee-options" role="radiogroup" aria-label="Fee tier">
<label class="fee-opt">
<input type="radio" name="fee" value="0.0005" />
<span class="fee-card">
<span class="fee-rate mono">0.05%</span>
<span class="fee-desc">Stable pairs</span>
<span class="fee-share mono">8% of TVL</span>
</span>
</label>
<label class="fee-opt">
<input type="radio" name="fee" value="0.003" checked />
<span class="fee-card">
<span class="fee-rate mono">0.30%</span>
<span class="fee-desc">Most pairs</span>
<span class="fee-share mono">87% of TVL</span>
</span>
</label>
<label class="fee-opt">
<input type="radio" name="fee" value="0.01" />
<span class="fee-card">
<span class="fee-rate mono">1.00%</span>
<span class="fee-desc">Exotic pairs</span>
<span class="fee-share mono">5% of TVL</span>
</span>
</label>
</div>
</fieldset>
<!-- Token A input -->
<div class="token-box">
<div class="token-box-top">
<label for="amount-eth">Deposit</label>
<span class="balance">Balance <span class="mono" id="bal-eth">4.2618</span>
<button class="max-btn" type="button" data-max="eth">MAX</button>
</span>
</div>
<div class="token-box-main">
<input class="amount mono" id="amount-eth" type="text" inputmode="decimal" placeholder="0.0" autocomplete="off" aria-label="ETH amount" />
<span class="token-tag">
<span class="coin coin-eth sm" aria-hidden="true">Ξ</span>
<span class="token-sym">ETH</span>
</span>
</div>
<div class="token-box-sub mono" id="usd-eth">≈ $0.00</div>
</div>
<div class="link-divider" aria-hidden="true">
<span class="link-icon">⇅</span>
<span class="link-note">ratio-linked</span>
</div>
<!-- Token B input -->
<div class="token-box">
<div class="token-box-top">
<label for="amount-usdc">Deposit</label>
<span class="balance">Balance <span class="mono" id="bal-usdc">12,840.55</span>
<button class="max-btn" type="button" data-max="usdc">MAX</button>
</span>
</div>
<div class="token-box-main">
<input class="amount mono" id="amount-usdc" type="text" inputmode="decimal" placeholder="0.0" autocomplete="off" aria-label="USDC amount" />
<span class="token-tag">
<span class="coin coin-usdc sm" aria-hidden="true">$</span>
<span class="token-sym">USDC</span>
</span>
</div>
<div class="token-box-sub mono" id="usd-usdc">≈ $0.00</div>
</div>
<!-- Estimates -->
<div class="estimates" aria-live="polite">
<div class="est-row">
<span>Pool share</span>
<span class="mono" id="est-share">0.0000%</span>
</div>
<div class="est-row">
<span>LP tokens received</span>
<span class="mono" id="est-lp">0.0000 LUM-LP</span>
</div>
<div class="est-row">
<span>Fee APR <span class="apr-badge" id="apr-badge">est.</span></span>
<span class="mono pos" id="est-apr">22.0%</span>
</div>
<div class="est-row est-sub">
<span>Network fee</span>
<span class="mono">~0.0012 ETH</span>
</div>
</div>
<button class="cta" id="btn-add" type="button" disabled>Enter an amount</button>
<p class="risk-note">Providing liquidity exposes you to <strong>impermanent loss</strong> if the pair price diverges. Simulation only.</p>
</div>
<!-- ---- REMOVE pane ---- -->
<div class="pane" id="pane-remove" role="tabpanel" aria-labelledby="tab-remove" hidden>
<div class="remove-head">
<span class="remove-label">Amount to remove</span>
<span class="remove-pct mono" id="remove-pct">25%</span>
</div>
<input class="pct-slider" id="remove-slider" type="range" min="0" max="100" value="25" step="1" aria-label="Percentage of position to remove" />
<div class="pct-presets" role="group" aria-label="Quick percentages">
<button type="button" data-pct="25">25%</button>
<button type="button" data-pct="50">50%</button>
<button type="button" data-pct="75">75%</button>
<button type="button" data-pct="100">MAX</button>
</div>
<div class="estimates" aria-live="polite">
<div class="est-row">
<span><span class="coin coin-eth sm inline" aria-hidden="true">Ξ</span> ETH you receive</span>
<span class="mono" id="rm-eth">0.4180</span>
</div>
<div class="est-row">
<span><span class="coin coin-usdc sm inline" aria-hidden="true">$</span> USDC you receive</span>
<span class="mono" id="rm-usdc">1,080.24</span>
</div>
<div class="est-row">
<span>LP tokens burned</span>
<span class="mono" id="rm-lp">10.5625 LUM-LP</span>
</div>
<div class="est-row est-sub">
<span>Unclaimed fees (auto-collected)</span>
<span class="mono pos">+$31.92</span>
</div>
</div>
<button class="cta cta-danger" id="btn-remove" type="button">Remove 25% liquidity</button>
<p class="risk-note">Removing liquidity stops fee accrual on the withdrawn share. Simulation only.</p>
</div>
</section>
<!-- ===== Right: Position summary ===== -->
<aside class="panel glass position" aria-label="Your position">
<div class="pos-head">
<h2>Your position</h2>
<span class="status-pill"><span class="status-dot"></span>In range</span>
</div>
<div class="pos-value">
<span class="pos-value-label">Position value</span>
<span class="pos-value-num mono" id="pos-value">$5,402.18</span>
<span class="pos-delta pos mono">+$112.40 (2.1%) past 7d</span>
</div>
<ul class="pos-tokens">
<li>
<span class="pos-token"><span class="coin coin-eth sm" aria-hidden="true">Ξ</span> ETH</span>
<span class="pos-amts">
<span class="mono" id="pos-eth">1.6720</span>
<span class="mono sub" id="pos-eth-usd">$4,320.97</span>
</span>
</li>
<li>
<span class="pos-token"><span class="coin coin-usdc sm" aria-hidden="true">$</span> USDC</span>
<span class="pos-amts">
<span class="mono" id="pos-usdc">4,320.97</span>
<span class="mono sub" id="pos-usdc-usd">$4,320.97</span>
</span>
</li>
</ul>
<div class="pos-meta">
<div class="pos-meta-row">
<span>LP tokens</span>
<span class="mono" id="pos-lp">42.2500 LUM-LP</span>
</div>
<div class="pos-meta-row">
<span>Pool share</span>
<span class="mono" id="pos-share">0.0112%</span>
</div>
<div class="pos-meta-row">
<span>Fees earned (lifetime)</span>
<span class="mono pos" id="pos-fees">$127.66</span>
</div>
<div class="pos-meta-row">
<span>Deposited</span>
<span class="mono">Mar 18, 2026</span>
</div>
</div>
<div class="apr-card">
<div class="apr-card-top">
<span>Fee APR (7d)</span>
<span class="mono apr-num" id="pos-apr">22.0%</span>
</div>
<div class="apr-bar" role="img" aria-label="APR vs pool average">
<span class="apr-bar-fill" style="width:62%"></span>
</div>
<span class="apr-foot">vs 17.4% pool average — based on 24h fees / TVL, not guaranteed</span>
</div>
</aside>
</div>
<!-- ===== Confirm modal ===== -->
<div class="modal-backdrop" id="modal" hidden>
<div class="modal glass" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-step" id="modal-confirm">
<h3 id="modal-title">Confirm add liquidity</h3>
<div class="confirm-pair">
<div class="confirm-token">
<span class="coin coin-eth" aria-hidden="true">Ξ</span>
<span class="mono confirm-amt" id="cf-eth">0.0000</span>
<span class="confirm-sym">ETH</span>
</div>
<span class="confirm-plus" aria-hidden="true">+</span>
<div class="confirm-token">
<span class="coin coin-usdc" aria-hidden="true">$</span>
<span class="mono confirm-amt" id="cf-usdc">0.00</span>
<span class="confirm-sym">USDC</span>
</div>
</div>
<div class="estimates modal-est">
<div class="est-row"><span>Fee tier</span><span class="mono" id="cf-fee">0.30%</span></div>
<div class="est-row"><span>Pool share</span><span class="mono" id="cf-share">0.0000%</span></div>
<div class="est-row"><span>LP tokens</span><span class="mono" id="cf-lp">0.0000 LUM-LP</span></div>
<div class="est-row"><span>Slippage tolerance</span><span class="mono">0.5%</span></div>
</div>
<div class="warn-box" role="note">
<span class="warn-icon" aria-hidden="true">⚠</span>
<span>You are signing a simulated transaction on <strong>Lumen Chain</strong>. Rates may shift if the pool moves before confirmation.</span>
</div>
<div class="modal-actions">
<button class="ghost-btn" id="btn-cancel" type="button">Cancel</button>
<button class="cta" id="btn-confirm" type="button">Sign & supply</button>
</div>
</div>
<div class="modal-step" id="modal-pending" hidden>
<div class="spinner" aria-hidden="true"></div>
<h3>Supplying liquidity…</h3>
<p class="pending-sub">Waiting for confirmation on Lumen Chain</p>
<span class="mono tx-hash" id="tx-hash">0x91c4…e7b2</span>
</div>
<div class="modal-step" id="modal-success" hidden>
<div class="success-ring" aria-hidden="true">✓</div>
<h3>Liquidity added</h3>
<p class="pending-sub">Your position has been updated</p>
<span class="mono tx-hash">0x91c4…e7b2 · block 18,442,901</span>
<div class="modal-actions">
<button class="cta" id="btn-done" type="button">Done</button>
</div>
</div>
</div>
</div>
<div class="toast-zone" id="toast-zone" aria-live="polite"></div>
</main>
<script src="script.js"></script>
</body>
</html>Liquidity Pool / Provide LP
A glassy, dark-first “provide liquidity” widget for a fictional ETH/USDC pool on Lumen Chain. The two deposit inputs are ratio-linked: type into either one and the other auto-fills from the mock pool price (1 ETH = 2,584.30 USDC). Each input shows the wallet balance with a MAX shortcut and a live fiat estimate, while the summary below recomputes your pool share, estimated LUM-LP tokens minted, and the fee-APR projection on every keystroke. A three-card fee-tier selector (0.05% / 0.3% / 1%) swaps in per-tier TVL, 24h volume, and fee stats with animated count-ups.
The card is split into Add and Remove tabs with a sliding glider. The Remove tab drives everything from a percent slider with 25/50/75/MAX presets, previewing the exact ETH, USDC, and burned LP amounts plus auto-collected fees. A sidebar position card tracks your pooled tokens, LP balance, pool share, lifetime fees, and a fee-APR bar compared to the pool average.
Pressing Add or Remove opens a three-step confirmation modal — review with an impermanent-loss warning, a simulated wallet-signing pending state with a fake transaction hash, then a success step that updates balances and the position and fires a toast. Everything is keyboard-accessible: Escape closes the modal, focus rings are visible, and the panel collapses cleanly down to 360px.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.