Banking — Investing
A trust-first fintech investing screen rendered in pure HTML, CSS and vanilla JavaScript. It pairs a headline portfolio value with day gain or loss, an interactive SVG performance chart with timeframe tabs and hover scrubbing, a holdings table, an allocation donut with legend, and a buy or sell trade panel that opens a confirmation modal. Clicking any holding reveals a detail sheet with cost basis and total gain. Tabular figures, masked security cues and toast feedback complete the experience.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.10);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 2px 6px rgba(14, 27, 58, 0.05);
--sh-2: 0 6px 18px rgba(14, 27, 58, 0.10), 0 2px 6px rgba(14, 27, 58, 0.06);
--sh-3: 0 18px 50px rgba(14, 27, 58, 0.22);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.num,
.big-amount,
.bp-amount,
.dc-value {
font-variant-numeric: tabular-nums;
}
.app {
max-width: 1100px;
margin: 0 auto;
padding: 18px clamp(12px, 3vw, 28px) 48px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 2px 18px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.logo {
width: 38px; height: 38px;
border-radius: 11px;
background: linear-gradient(135deg, var(--navy), var(--accent));
color: #fff;
display: grid; place-items: center;
box-shadow: var(--sh-1);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 15px; font-weight: 700; color: var(--navy); }
.badge-verified {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; font-weight: 600; color: var(--ok);
margin-top: 2px;
}
.topbar-right { display: flex; align-items: center; gap: 10px; }
.icon-btn {
width: 38px; height: 38px;
border: 1px solid var(--line);
background: var(--surface);
border-radius: 11px;
color: var(--ink-2);
cursor: pointer;
display: grid; place-items: center;
transition: border-color .15s, color .15s, transform .1s;
}
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
.icon-btn:active { transform: scale(.95); }
.avatar {
width: 38px; height: 38px;
border-radius: 50%;
background: var(--accent-50);
color: var(--accent-d);
font-weight: 700; font-size: 13px;
display: grid; place-items: center;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 18px;
align-items: start;
}
.col-main, .col-side { display: flex; flex-direction: column; gap: 18px; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 18px;
}
.card-head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 14px;
}
.card-head h2 { margin: 0; font-size: 15px; font-weight: 700; color: var(--navy); }
.muted-sm { font-size: 12px; color: var(--muted); font-weight: 500; }
.label { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
/* ---------- Hero ---------- */
.hero {
background:
radial-gradient(120% 120% at 100% 0%, rgba(59,110,246,0.08), transparent 55%),
var(--surface);
}
.hero-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; margin-bottom: 16px;
}
.big-amount {
font-size: clamp(28px, 5vw, 38px);
font-weight: 800;
color: var(--ink);
letter-spacing: -0.02em;
margin: 4px 0 6px;
}
.delta {
display: inline-flex; align-items: center; gap: 6px;
font-size: 14px; font-weight: 700;
padding: 3px 9px 3px 6px;
border-radius: 999px;
}
.delta.up { color: var(--ok); background: rgba(31,157,98,0.10); }
.delta.down { color: var(--danger); background: rgba(212,73,62,0.10); }
.delta.down svg { transform: rotate(180deg); }
.delta-pct { font-weight: 600; opacity: .9; }
.delta-period { font-weight: 500; color: var(--muted); }
.buying-power { text-align: right; }
.bp-amount { font-size: 17px; font-weight: 700; color: var(--navy); margin-top: 4px; }
/* ---------- Chart ---------- */
.chart-tabs {
display: flex; gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
padding: 4px; border-radius: 12px;
width: max-content;
margin-bottom: 14px;
}
.tab {
border: 0; background: transparent;
font: inherit; font-size: 12px; font-weight: 600;
color: var(--muted);
padding: 5px 12px; border-radius: 8px;
cursor: pointer; transition: background .15s, color .15s;
}
.tab:hover { color: var(--ink); }
.tab.is-active { background: var(--surface); color: var(--accent-d); box-shadow: var(--sh-1); }
.chart-wrap { position: relative; }
.chart { width: 100%; height: 190px; display: block; overflow: visible; }
.chart-area { fill: url(#fillGrad); transition: d .35s ease; }
.chart-line {
fill: none; stroke: var(--accent); stroke-width: 2.4;
stroke-linecap: round; stroke-linejoin: round;
transition: d .35s ease, stroke .25s;
}
.chart-dot { fill: var(--accent); stroke: #fff; stroke-width: 2; opacity: 0; transition: opacity .15s; }
.chart-tip {
position: absolute; pointer-events: none;
transform: translate(-50%, -120%);
background: var(--navy); color: #fff;
font-size: 12px; font-weight: 600;
padding: 5px 9px; border-radius: 8px;
white-space: nowrap; box-shadow: var(--sh-2);
font-variant-numeric: tabular-nums;
}
.chart-tip::after {
content: ""; position: absolute; left: 50%; top: 100%;
transform: translateX(-50%);
border: 5px solid transparent; border-top-color: var(--navy);
}
/* ---------- Holdings table ---------- */
.table { display: flex; flex-direction: column; }
.thead, .hrow {
display: grid;
grid-template-columns: 1fr 78px 110px 92px;
align-items: center;
gap: 8px;
}
.thead {
font-size: 11px; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: .04em;
padding: 0 6px 8px;
border-bottom: 1px solid var(--line);
}
.thead .num, .hrow .num { text-align: right; }
.hrow {
padding: 11px 6px;
border-bottom: 1px solid var(--line);
cursor: pointer;
border-radius: 10px;
transition: background .12s;
}
.hrow:last-child { border-bottom: 0; }
.hrow:hover { background: var(--accent-50); }
.asset { display: flex; align-items: center; gap: 10px; min-width: 0; }
.tkr-badge {
width: 34px; height: 34px; flex: none;
border-radius: 9px;
display: grid; place-items: center;
font-size: 12px; font-weight: 800; color: #fff;
letter-spacing: -.02em;
}
.asset-meta { min-width: 0; }
.asset-ticker { font-weight: 700; font-size: 14px; color: var(--ink); }
.asset-name {
font-size: 12px; color: var(--muted);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hrow .shares { font-size: 13px; color: var(--ink-2); }
.hrow .value { font-weight: 700; font-size: 14px; color: var(--ink); }
.chg { font-size: 13px; font-weight: 700; }
.chg.up { color: var(--ok); }
.chg.down { color: var(--danger); }
/* ---------- Donut ---------- */
.donut-wrap { position: relative; width: 168px; margin: 4px auto 14px; }
.donut { width: 168px; height: 168px; transform: rotate(-90deg); }
.donut .seg { transition: stroke-width .15s; cursor: pointer; }
.donut .seg:hover { stroke-width: 16; }
.donut-center {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
pointer-events: none;
}
.dc-label { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.dc-value { font-size: 18px; font-weight: 800; color: var(--navy); }
.legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.legend li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.legend .dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.legend .lg-name { font-weight: 600; color: var(--ink); }
.legend .lg-pct { margin-left: auto; font-weight: 700; color: var(--ink-2); font-variant-numeric: tabular-nums; }
/* ---------- Trade panel ---------- */
.seg {
display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
background: var(--bg); border: 1px solid var(--line);
padding: 4px; border-radius: 12px; margin-bottom: 14px;
}
.seg-btn {
border: 0; background: transparent; font: inherit;
font-size: 13px; font-weight: 700; color: var(--muted);
padding: 8px; border-radius: 9px; cursor: pointer;
transition: background .15s, color .15s;
}
.seg-btn.is-active { background: var(--surface); box-shadow: var(--sh-1); }
.seg-btn.is-active[data-side="buy"] { color: var(--ok); }
.seg-btn.is-active[data-side="sell"] { color: var(--danger); }
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.field > span { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.field select, .field input {
font: inherit; font-size: 14px;
padding: 10px 11px;
border: 1px solid var(--line-2);
border-radius: 10px;
background: var(--surface);
color: var(--ink);
width: 100%;
transition: border-color .15s, box-shadow .15s;
}
.field input { font-variant-numeric: tabular-nums; }
.field select:focus, .field input:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-50);
}
.trade-est {
display: flex; align-items: center; justify-content: space-between;
padding: 11px 12px; margin-bottom: 12px;
background: var(--bg); border-radius: 10px;
font-size: 13px; color: var(--ink-2);
}
.trade-est strong { font-size: 16px; color: var(--ink); }
.trade-note {
display: flex; align-items: center; gap: 5px;
font-size: 11px; color: var(--muted); margin: 10px 0 0;
}
/* ---------- Buttons ---------- */
.btn {
font: inherit; font-weight: 700; font-size: 14px;
border-radius: 11px; cursor: pointer;
padding: 11px 16px; border: 1px solid transparent;
transition: transform .1s, background .15s, box-shadow .15s, border-color .15s;
}
.btn:active { transform: translateY(1px); }
.btn.primary {
width: 100%;
background: var(--accent); color: #fff;
box-shadow: 0 6px 16px rgba(59,110,246,0.30);
}
.btn.primary:hover { background: var(--accent-d); }
.btn.ghost {
background: var(--surface); color: var(--ink-2);
border-color: var(--line-2);
}
.btn.ghost:hover { border-color: var(--ink-2); }
/* ---------- Modals ---------- */
.modal-backdrop {
position: fixed; inset: 0;
background: rgba(14,27,58,0.45);
backdrop-filter: blur(2px);
display: grid; place-items: center;
padding: 16px; z-index: 50;
animation: fade .15s ease;
}
.modal {
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
width: min(420px, 100%);
padding: 22px;
position: relative;
animation: pop .18s cubic-bezier(.2,.9,.3,1.2);
}
@keyframes fade { from { opacity: 0; } }
@keyframes pop { from { opacity: 0; transform: translateY(10px) scale(.97); } }
.modal h3 { margin: 0 0 14px; font-size: 18px; color: var(--navy); }
.modal-x {
position: absolute; top: 12px; right: 14px;
border: 0; background: transparent;
font-size: 24px; line-height: 1; color: var(--muted);
cursor: pointer; width: 30px; height: 30px; border-radius: 8px;
}
.modal-x:hover { background: var(--bg); color: var(--ink); }
.confirm-rows { display: flex; flex-direction: column; gap: 0; margin-bottom: 18px; }
.crow {
display: flex; align-items: center; justify-content: space-between;
padding: 11px 0; border-bottom: 1px solid var(--line);
font-size: 14px;
}
.crow:last-child { border-bottom: 0; }
.crow span { color: var(--muted); font-weight: 500; }
.crow strong { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.crow.total strong { font-size: 17px; }
.modal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.detail-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.detail-head .tkr-badge { width: 44px; height: 44px; font-size: 14px; border-radius: 12px; }
.detail-head h3 { margin: 0; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.dcell {
background: var(--bg); border-radius: 12px; padding: 12px 14px;
}
.dcell .dlabel { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
.dcell .dval { font-size: 16px; font-weight: 700; color: var(--ink); margin-top: 3px; font-variant-numeric: tabular-nums; }
.dcell .dval.up { color: var(--ok); }
.dcell .dval.down { color: var(--danger); }
/* ---------- Toast ---------- */
.toast-host {
position: fixed; left: 50%; bottom: 24px;
transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px;
z-index: 80; align-items: center;
}
.toast {
background: var(--navy); color: #fff;
font-size: 13px; font-weight: 600;
padding: 11px 16px; border-radius: 12px;
box-shadow: var(--sh-3);
display: flex; align-items: center; gap: 8px;
animation: toastIn .25s ease;
}
.toast.ok { background: var(--ok); }
.toast.sell { background: var(--navy-2); }
@keyframes toastIn { from { opacity: 0; transform: translateY(14px); } }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.col-side { flex-direction: column; }
}
@media (max-width: 520px) {
.app { padding: 14px 12px 40px; }
.hero-head { flex-direction: column; gap: 10px; }
.buying-power { text-align: left; }
.chart-tabs { width: 100%; justify-content: space-between; }
.tab { flex: 1; padding: 6px 4px; text-align: center; }
.thead, .hrow { grid-template-columns: 1fr 70px 84px; }
.thead span:nth-child(2), .hrow .shares { display: none; }
.detail-grid { grid-template-columns: 1fr 1fr; }
.modal-actions { grid-template-columns: 1fr; }
.big-amount { font-size: 30px; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var holdings = [
{ ticker: "VOO", name: "Vanguard S&P 500 ETF", color: "#3b6ef6", shares: 92, price: 512.34, chgPct: 0.84 },
{ ticker: "AAPL", name: "Apple Inc.", color: "#0fb5a6", shares: 140, price: 231.18, chgPct: 1.62 },
{ ticker: "MSFT", name: "Microsoft Corp.", color: "#7c5cff", shares: 58, price: 448.90, chgPct: -0.41 },
{ ticker: "NVDA", name: "NVIDIA Corp.", color: "#d9982b", shares: 96, price: 128.55, chgPct: 3.27 },
{ ticker: "BTC", name: "Bitcoin", color: "#16264d", shares: 0.42, price: 64210.0, chgPct: -1.18 }
];
var buyingPower = 6540.0;
var fmt = function (n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
var fmt0 = function (n) {
return "$" + Math.round(n).toLocaleString("en-US");
};
var pct = function (n) {
return (n >= 0 ? "+" : "") + n.toFixed(2) + "%";
};
var val = function (h) { return h.shares * h.price; };
/* ---------- Toast ---------- */
var toastHost = document.getElementById("toastHost");
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.textContent = msg;
toastHost.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(10px)";
setTimeout(function () { el.remove(); }, 320);
}, 2400);
}
/* ---------- Render holdings ---------- */
var holdingsBody = document.getElementById("holdingsBody");
function renderHoldings() {
holdingsBody.innerHTML = "";
holdings.forEach(function (h, i) {
var v = val(h);
var row = document.createElement("div");
row.className = "hrow";
row.setAttribute("role", "row");
row.tabIndex = 0;
var up = h.chgPct >= 0;
row.innerHTML =
'<div class="asset">' +
'<div class="tkr-badge" style="background:' + h.color + '">' + h.ticker.slice(0, 4) + "</div>" +
'<div class="asset-meta">' +
'<div class="asset-ticker">' + h.ticker + "</div>" +
'<div class="asset-name">' + h.name + "</div>" +
"</div>" +
"</div>" +
'<div class="num shares">' + (h.shares < 1 ? h.shares.toFixed(2) : h.shares) + "</div>" +
'<div class="num value">' + fmt(v) + "</div>" +
'<div class="num chg ' + (up ? "up" : "down") + '">' + pct(h.chgPct) + "</div>";
row.addEventListener("click", function () { openDetail(i); });
row.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openDetail(i); }
});
holdingsBody.appendChild(row);
});
document.getElementById("holdingsCount").textContent = holdings.length + " positions";
}
/* ---------- Portfolio summary ---------- */
function totalValue() {
return holdings.reduce(function (s, h) { return s + val(h); }, 0);
}
function renderSummary() {
var holdVal = totalValue();
var port = holdVal + buyingPower;
// weighted day change
var dayGain = holdings.reduce(function (s, h) {
var v = val(h);
return s + v - v / (1 + h.chgPct / 100);
}, 0);
var dayPct = (dayGain / (holdVal - dayGain)) * 100;
document.getElementById("portfolioValue").textContent = fmt(port);
var d = document.getElementById("portfolioDelta");
var up = dayGain >= 0;
d.className = "delta " + (up ? "up" : "down");
document.getElementById("portfolioDeltaAmt").textContent = (up ? "+" : "-") + fmt(Math.abs(dayGain)).replace("$", "$");
document.getElementById("portfolioDeltaPct").textContent = pct(dayPct);
document.getElementById("donutTotal").textContent = fmt0(holdVal);
}
/* ---------- Allocation donut ---------- */
var donut = document.getElementById("donut");
var legend = document.getElementById("legend");
function renderDonut() {
var total = totalValue();
var R = 52, C = 60, circ = 2 * Math.PI * R;
donut.innerHTML = "";
legend.innerHTML = "";
var offset = 0;
holdings.forEach(function (h) {
var frac = val(h) / total;
var len = frac * circ;
var c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
c.setAttribute("class", "seg");
c.setAttribute("cx", C); c.setAttribute("cy", C); c.setAttribute("r", R);
c.setAttribute("fill", "none");
c.setAttribute("stroke", h.color);
c.setAttribute("stroke-width", "13");
c.setAttribute("stroke-dasharray", len + " " + (circ - len));
c.setAttribute("stroke-dashoffset", -offset);
var title = document.createElementNS("http://www.w3.org/2000/svg", "title");
title.textContent = h.ticker + " — " + (frac * 100).toFixed(1) + "%";
c.appendChild(title);
donut.appendChild(c);
offset += len;
var li = document.createElement("li");
li.innerHTML =
'<span class="dot" style="background:' + h.color + '"></span>' +
'<span class="lg-name">' + h.ticker + "</span>" +
'<span class="lg-pct">' + (frac * 100).toFixed(1) + "%</span>";
legend.appendChild(li);
});
}
/* ---------- Line chart ---------- */
var chartLine = document.getElementById("chartLine");
var chartArea = document.getElementById("chartArea");
var chartDot = document.getElementById("chartDot");
var chartTip = document.getElementById("chartTip");
var chartSvg = document.getElementById("chart");
var W = 600, H = 200;
var rangeMeta = {
"1D": { n: 24, vol: 0.4, drift: 2.2, seed: 11, label: "today" },
"1W": { n: 28, vol: 0.9, drift: 2.2, seed: 23, label: "this week" },
"1M": { n: 30, vol: 1.4, drift: 4.0, seed: 31, label: "past month" },
"6M": { n: 26, vol: 2.6, drift: 9.0, seed: 47, label: "past 6 months" },
"1Y": { n: 24, vol: 3.4, drift: 14.0, seed: 53, label: "past year" },
"ALL":{ n: 30, vol: 4.2, drift: 26.0, seed: 67, label: "all time" }
};
function seeded(seed) {
var s = seed;
return function () {
s = (s * 9301 + 49297) % 233280;
return s / 233280;
};
}
var currentPoints = [];
function buildSeries(range) {
var m = rangeMeta[range];
var rnd = seeded(m.seed);
var base = totalValue() / (1 + m.drift / 100);
var pts = [];
var v = base;
for (var i = 0; i < m.n; i++) {
var t = i / (m.n - 1);
var trend = base * (m.drift / 100) * t;
var noise = (rnd() - 0.45) * base * (m.vol / 100);
v = base + trend + noise;
pts.push(v);
}
pts[pts.length - 1] = totalValue(); // anchor to current
return pts;
}
function pathFor(pts) {
var min = Math.min.apply(null, pts);
var max = Math.max.apply(null, pts);
var pad = (max - min) * 0.18 || 1;
min -= pad; max += pad;
var coords = pts.map(function (p, i) {
var x = (i / (pts.length - 1)) * W;
var y = H - ((p - min) / (max - min)) * H;
return [x, y];
});
var line = coords.map(function (c, i) {
return (i === 0 ? "M" : "L") + c[0].toFixed(1) + " " + c[1].toFixed(1);
}).join(" ");
var area = line + " L" + W + " " + H + " L0 " + H + " Z";
return { line: line, area: area, coords: coords };
}
function drawChart(range) {
currentPoints = buildSeries(range);
var p = pathFor(currentPoints);
chartLine.setAttribute("d", p.line);
chartArea.setAttribute("d", p.area);
chartLine._coords = p.coords;
var up = currentPoints[currentPoints.length - 1] >= currentPoints[0];
chartLine.style.stroke = up ? "var(--accent)" : "var(--danger)";
var m = rangeMeta[range];
document.getElementById("portfolioDeltaPeriod").textContent = m.label;
}
// hover scrubbing
function onMove(evt) {
var coords = chartLine._coords;
if (!coords) return;
var rect = chartSvg.getBoundingClientRect();
var clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
var relX = (clientX - rect.left) / rect.width;
var idx = Math.round(relX * (coords.length - 1));
idx = Math.max(0, Math.min(coords.length - 1, idx));
var c = coords[idx];
var px = (c[0] / W) * rect.width;
var py = (c[1] / H) * rect.height;
chartDot.setAttribute("cx", c[0]);
chartDot.setAttribute("cy", c[1]);
chartDot.style.opacity = "1";
chartTip.hidden = false;
chartTip.textContent = fmt(currentPoints[idx]);
chartTip.style.left = px + "px";
chartTip.style.top = py + "px";
}
function onLeave() {
chartDot.style.opacity = "0";
chartTip.hidden = true;
}
chartSvg.addEventListener("mousemove", onMove);
chartSvg.addEventListener("mouseleave", onLeave);
chartSvg.addEventListener("touchmove", function (e) { onMove(e); }, { passive: true });
chartSvg.addEventListener("touchend", onLeave);
// timeframe tabs
var tabs = document.querySelectorAll(".tab");
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) { t.classList.remove("is-active"); t.removeAttribute("aria-selected"); });
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
drawChart(tab.dataset.range);
});
});
/* ---------- Trade panel ---------- */
var tradeSide = "buy";
var tradeAsset = document.getElementById("tradeAsset");
var tradeShares = document.getElementById("tradeShares");
var tradeEst = document.getElementById("tradeEst");
var estSide = document.getElementById("estSide");
function fillAssetSelect() {
tradeAsset.innerHTML = "";
holdings.forEach(function (h, i) {
var o = document.createElement("option");
o.value = i;
o.textContent = h.ticker + " · " + fmt(h.price);
tradeAsset.appendChild(o);
});
}
function selectedHolding() { return holdings[+tradeAsset.value] || holdings[0]; }
function updateEst() {
var h = selectedHolding();
var sh = parseFloat(tradeShares.value) || 0;
tradeEst.textContent = fmt(sh * h.price);
estSide.textContent = tradeSide === "buy" ? "cost" : "proceeds";
}
document.querySelectorAll(".seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".seg-btn").forEach(function (b) { b.classList.remove("is-active"); });
btn.classList.add("is-active");
tradeSide = btn.dataset.side;
updateEst();
});
});
tradeAsset.addEventListener("change", updateEst);
tradeShares.addEventListener("input", updateEst);
/* ---------- Trade modal ---------- */
var tradeModal = document.getElementById("tradeModal");
var confirmRows = document.getElementById("confirmRows");
var lastClicked = null;
function openModal(modal, opener) {
lastClicked = opener || document.activeElement;
modal.hidden = false;
var f = modal.querySelector(".btn.primary, .modal-x");
if (f) f.focus();
}
function closeModal(modal) {
modal.hidden = true;
if (lastClicked && lastClicked.focus) lastClicked.focus();
}
document.getElementById("reviewBtn").addEventListener("click", function () {
var h = selectedHolding();
var sh = parseFloat(tradeShares.value) || 0;
if (sh <= 0) { toast("Enter a share amount", "sell"); return; }
var amount = sh * h.price;
if (tradeSide === "buy" && amount > buyingPower) {
toast("Insufficient buying power", "sell"); return;
}
if (tradeSide === "sell" && sh > h.shares) {
toast("You only hold " + h.shares + " " + h.ticker, "sell"); return;
}
var fee = Math.max(0, amount * 0.0005);
confirmRows.innerHTML =
crow("Side", (tradeSide === "buy" ? "Buy" : "Sell")) +
crow("Asset", h.ticker + " — " + h.name) +
crow("Shares", String(sh)) +
crow("Market price", fmt(h.price)) +
crow("Est. fee", fmt(fee)) +
crow(tradeSide === "buy" ? "Total cost" : "Net proceeds",
fmt(tradeSide === "buy" ? amount + fee : amount - fee), true);
openModal(tradeModal, this);
});
function crow(label, value, total) {
return '<div class="crow' + (total ? " total" : "") + '"><span>' + label + "</span><strong>" + value + "</strong></div>";
}
document.getElementById("tradeConfirm").addEventListener("click", function () {
var h = selectedHolding();
var sh = parseFloat(tradeShares.value) || 0;
var amount = sh * h.price;
if (tradeSide === "buy") {
h.shares += sh;
buyingPower -= amount;
toast("Bought " + sh + " " + h.ticker + " for " + fmt(amount), "ok");
} else {
h.shares = Math.max(0, h.shares - sh);
buyingPower += amount;
toast("Sold " + sh + " " + h.ticker + " for " + fmt(amount), "sell");
}
document.querySelector(".bp-amount").textContent = fmt(buyingPower);
closeModal(tradeModal);
renderHoldings();
renderSummary();
renderDonut();
fillAssetSelect();
updateEst();
drawChart(document.querySelector(".tab.is-active").dataset.range);
});
document.getElementById("tradeClose").addEventListener("click", function () { closeModal(tradeModal); });
document.getElementById("tradeCancel").addEventListener("click", function () { closeModal(tradeModal); });
/* ---------- Holding detail modal ---------- */
var detailModal = document.getElementById("detailModal");
function openDetail(i) {
var h = holdings[i];
var v = val(h);
var costBasis = h.price / (1 + (h.chgPct + 4.6) / 100); // fictional avg cost
var totalGain = (h.price - costBasis) * h.shares;
var totalGainPct = ((h.price - costBasis) / costBasis) * 100;
var badge = document.getElementById("detailBadge");
badge.style.background = h.color;
badge.textContent = h.ticker.slice(0, 4);
document.getElementById("detailTitle").textContent = h.ticker;
document.getElementById("detailName").textContent = h.name;
var up = h.chgPct >= 0, tup = totalGain >= 0;
document.getElementById("detailGrid").innerHTML =
dcell("Market value", fmt(v)) +
dcell("Shares", String(h.shares)) +
dcell("Last price", fmt(h.price)) +
dcell("Today", pct(h.chgPct), up) +
dcell("Avg cost", fmt(costBasis)) +
dcell("Total gain", (tup ? "+" : "-") + fmt(Math.abs(totalGain)) + " (" + pct(totalGainPct) + ")", tup);
tradeAsset.value = i;
updateEst();
openModal(detailModal);
}
function dcell(label, value, up) {
var cls = up === undefined ? "" : (up ? " up" : " down");
return '<div class="dcell"><div class="dlabel">' + label + '</div><div class="dval' + cls + '">' + value + "</div></div>";
}
document.getElementById("detailClose").addEventListener("click", function () { closeModal(detailModal); });
/* ---------- Global modal dismiss ---------- */
[tradeModal, detailModal].forEach(function (m) {
m.addEventListener("click", function (e) { if (e.target === m) closeModal(m); });
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
if (!tradeModal.hidden) closeModal(tradeModal);
if (!detailModal.hidden) closeModal(detailModal);
}
});
document.getElementById("lockBtn").addEventListener("click", function () {
toast("Session secured · 2FA active", "ok");
});
/* ---------- Init ---------- */
renderHoldings();
renderSummary();
renderDonut();
fillAssetSelect();
updateEst();
drawChart("1W");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Investing</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M4 18V9m5 9V5m5 13v-7m5 7V8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</div>
<div class="brand-text">
<strong>Northbank Invest</strong>
<span class="badge-verified">
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6.5 10.6 3.9 8l-1 1L6.5 12.6 13 6.1l-1-1z" fill="currentColor"/></svg>
Verified account
</span>
</div>
</div>
<div class="topbar-right">
<button class="icon-btn" id="lockBtn" aria-label="Session secured with 2FA" title="Secured with 2FA">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg>
</button>
<div class="avatar" aria-hidden="true">AR</div>
</div>
</header>
<main class="layout">
<section class="col-main">
<!-- Portfolio summary -->
<div class="card hero">
<div class="hero-head">
<div>
<div class="label">Portfolio value</div>
<div class="big-amount" id="portfolioValue">$148,920.45</div>
<div class="delta up" id="portfolioDelta">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M8 3.5 13 9H9.5v4h-3V9H3z" fill="currentColor"/></svg>
<span id="portfolioDeltaAmt">+$3,214.08</span>
<span class="delta-pct" id="portfolioDeltaPct">+2.21%</span>
<span class="delta-period" id="portfolioDeltaPeriod">today</span>
</div>
</div>
<div class="buying-power">
<div class="label">Buying power</div>
<div class="bp-amount">$6,540.00</div>
</div>
</div>
<div class="chart-tabs" role="tablist" aria-label="Chart timeframe">
<button class="tab" role="tab" data-range="1D">1D</button>
<button class="tab is-active" role="tab" aria-selected="true" data-range="1W">1W</button>
<button class="tab" role="tab" data-range="1M">1M</button>
<button class="tab" role="tab" data-range="6M">6M</button>
<button class="tab" role="tab" data-range="1Y">1Y</button>
<button class="tab" role="tab" data-range="ALL">ALL</button>
</div>
<div class="chart-wrap">
<svg id="chart" class="chart" viewBox="0 0 600 200" preserveAspectRatio="none" role="img" aria-label="Portfolio performance chart">
<defs>
<linearGradient id="fillGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.22"/>
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
</linearGradient>
</defs>
<path id="chartArea" class="chart-area" d="" />
<path id="chartLine" class="chart-line" d="" />
<circle id="chartDot" class="chart-dot" r="4" cx="0" cy="0" />
</svg>
<div class="chart-tip" id="chartTip" hidden></div>
</div>
</div>
<!-- Holdings -->
<div class="card">
<div class="card-head">
<h2>Holdings</h2>
<span class="muted-sm" id="holdingsCount">5 positions</span>
</div>
<div class="table" role="table" aria-label="Holdings">
<div class="thead" role="row">
<span role="columnheader">Asset</span>
<span role="columnheader" class="num">Shares</span>
<span role="columnheader" class="num">Value</span>
<span role="columnheader" class="num">Today</span>
</div>
<div id="holdingsBody"></div>
</div>
</div>
</section>
<aside class="col-side">
<!-- Allocation donut -->
<div class="card">
<div class="card-head"><h2>Allocation</h2></div>
<div class="donut-wrap">
<svg class="donut" viewBox="0 0 120 120" id="donut" role="img" aria-label="Portfolio allocation by holding"></svg>
<div class="donut-center">
<span class="dc-label">Total</span>
<span class="dc-value" id="donutTotal">$142,380</span>
</div>
</div>
<ul class="legend" id="legend"></ul>
</div>
<!-- Trade panel -->
<div class="card trade">
<div class="card-head"><h2>Trade</h2></div>
<div class="seg" role="group" aria-label="Order side">
<button class="seg-btn is-active" data-side="buy">Buy</button>
<button class="seg-btn" data-side="sell">Sell</button>
</div>
<label class="field">
<span>Asset</span>
<select id="tradeAsset"></select>
</label>
<label class="field">
<span>Shares</span>
<input id="tradeShares" type="number" min="0" step="0.01" value="1" inputmode="decimal" />
</label>
<div class="trade-est">
<span>Estimated <span id="estSide">cost</span></span>
<strong id="tradeEst" class="num">$0.00</strong>
</div>
<button class="btn primary" id="reviewBtn">Review order</button>
<p class="trade-note">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg>
Orders execute at next market price.
</p>
</div>
</aside>
</main>
</div>
<!-- Trade confirm modal -->
<div class="modal-backdrop" id="tradeModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="tradeModalTitle">
<button class="modal-x" id="tradeClose" aria-label="Close">×</button>
<h3 id="tradeModalTitle">Confirm order</h3>
<div class="confirm-rows" id="confirmRows"></div>
<div class="modal-actions">
<button class="btn ghost" id="tradeCancel">Cancel</button>
<button class="btn primary" id="tradeConfirm">Place order</button>
</div>
</div>
</div>
<!-- Holding detail modal -->
<div class="modal-backdrop" id="detailModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="detailTitle">
<button class="modal-x" id="detailClose" aria-label="Close">×</button>
<div class="detail-head">
<div class="tkr-badge" id="detailBadge"></div>
<div>
<h3 id="detailTitle">—</h3>
<span class="muted-sm" id="detailName">—</span>
</div>
</div>
<div class="detail-grid" id="detailGrid"></div>
</div>
</div>
<div class="toast-host" id="toastHost" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Investing
A self-contained investing dashboard for a fictional brokerage, “Northbank Invest.” The hero card leads with the total portfolio value, a colored day gain/loss pill, and the available buying power, all set in tabular figures so columns of money stay aligned. Below it, a timeframe tab strip (1D through ALL) redraws a smooth SVG area chart; moving the pointer across the chart scrubs a tooltip and dot to the nearest data point, and the line turns red when a range closes down.
The holdings table lists five positions with ticker badges, share counts, market value, and today’s percentage change. Selecting a row — by click or keyboard — opens a detail modal showing market value, average cost, last price, and total gain. On the right, an allocation donut and legend break the portfolio down by holding, and a trade panel lets you switch between buy and sell, pick an asset, enter shares, and preview the estimated cost or proceeds in real time.
Submitting a trade opens a confirmation modal with an itemized fee breakdown; placing the order mutates the underlying portfolio, adjusts buying power, and re-renders the table, summary, donut, and chart together, then fires a toast. Guardrails reject oversized buys against buying power and sells beyond the held quantity, and a lock control reinforces the 2FA security cue. Everything is keyboard-usable and reflows cleanly down to a 360px mobile viewport.
Illustrative UI only — not real banking software or financial advice.