Shop — Store Dashboard
A merchant store dashboard for the fictional Lumen Goods shop, built as a single self-contained page. Four KPI cards show net sales, orders, average order value and conversion with up or down deltas and inline SVG sparklines. An animated revenue area chart with a previous-period overlay sits beside an orders-by-status donut, a sortable top-products table, a low-stock reorder list and a live recent-orders feed. A period switcher recomputes every metric and redraws all charts.
MCP
Code
:root {
--bg: #f5f6f9;
--surface: #ffffff;
--ink: #16181d;
--ink-soft: #3a3f4a;
--muted: #6b7280;
--brand: #3457ff;
--brand-d: #2742d6;
--brand-tint: #eef1ff;
--sale: #e0245e;
--ok: #1f9d55;
--warn: #d97706;
--line: rgba(16, 18, 29, .1);
--line-soft: rgba(16, 18, 29, .06);
--shadow: 0 1px 2px rgba(16, 18, 29, .04), 0 8px 24px rgba(16, 18, 29, .06);
--radius: 16px;
--sb: 246px;
--c-1: #3457ff;
--c-2: #1f9d55;
--c-3: #d97706;
--c-4: #8b5cf6;
--c-5: #e0245e;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2 { margin: 0; font-weight: 700; letter-spacing: -.01em; }
p { margin: 0; }
button { font-family: inherit; cursor: pointer; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- App shell ---------- */
.app {
display: grid;
grid-template-columns: var(--sb) 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: #0f1320;
color: #c7ccda;
padding: 22px 16px;
display: flex;
flex-direction: column;
gap: 6px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 8px 18px;
color: #fff;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px; height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--brand), #6d8bff);
color: #fff;
}
.brand-name { font-size: 1.05rem; font-weight: 600; letter-spacing: -.01em; }
.brand-name strong { font-weight: 800; }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-link {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
color: #aab1c4;
text-decoration: none;
font-size: .92rem;
font-weight: 500;
transition: background .15s, color .15s;
}
.nav-link svg { flex: none; opacity: .85; }
.nav-link:hover { background: rgba(255, 255, 255, .06); color: #fff; }
.nav-link.is-active {
background: linear-gradient(135deg, rgba(52, 87, 255, .9), rgba(52, 87, 255, .65));
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08);
}
.nav-badge {
margin-left: auto;
background: var(--sale);
color: #fff;
font-size: .68rem;
font-weight: 700;
padding: 1px 7px;
border-radius: 999px;
}
.store-card {
margin-top: auto;
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 12px;
background: rgba(255, 255, 255, .05);
}
.store-avatar {
width: 36px; height: 36px;
border-radius: 9px;
display: grid; place-items: center;
font-size: .78rem; font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #8b5cf6, #3457ff);
}
.store-meta { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.store-name { color: #fff; font-size: .85rem; font-weight: 600; }
.store-plan { color: #8a91a6; font-size: .72rem; }
.dot-live {
margin-left: auto;
width: 8px; height: 8px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px rgba(31, 157, 85, .25);
}
/* ---------- Main ---------- */
.main { min-width: 0; display: flex; flex-direction: column; }
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: rgba(245, 246, 249, .85);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
padding: 16px clamp(16px, 3vw, 32px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.topbar-head { display: flex; align-items: center; gap: 12px; min-width: 0; }
.topbar h1 { font-size: 1.3rem; }
.topbar-sub { color: var(--muted); font-size: .85rem; }
.menu-btn {
display: none;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 10px;
padding: 8px;
color: var(--ink);
}
.topbar-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.period {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
}
.period-btn {
border: 0;
background: transparent;
color: var(--muted);
font-size: .82rem;
font-weight: 600;
padding: 6px 14px;
border-radius: 999px;
transition: background .15s, color .15s;
}
.period-btn:hover { color: var(--ink); }
.period-btn.is-active {
background: var(--ink);
color: #fff;
}
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 7px;
background: var(--surface);
border: 1px solid var(--line);
color: var(--ink);
font-size: .85rem;
font-weight: 600;
padding: 8px 14px;
border-radius: 10px;
transition: border-color .15s, box-shadow .15s, transform .05s;
}
.ghost-btn:hover { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint); }
.ghost-btn:active { transform: translateY(1px); }
.content {
padding: clamp(16px, 3vw, 32px);
display: flex;
flex-direction: column;
gap: 20px;
}
/* ---------- KPI cards ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line-soft);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 16px 18px;
display: grid;
grid-template-rows: auto auto 1fr;
gap: 6px;
position: relative;
overflow: hidden;
}
.kpi::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--brand);
opacity: .85;
}
.kpi[data-kpi="orders"]::before { background: var(--c-2); }
.kpi[data-kpi="aov"]::before { background: var(--c-3); }
.kpi[data-kpi="conv"]::before { background: var(--c-4); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.kpi-label { color: var(--muted); font-size: .82rem; font-weight: 600; }
.kpi-value { font-size: 1.7rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
.kpi-delta {
font-size: .76rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 3px;
white-space: nowrap;
}
.kpi-delta.up { color: var(--ok); background: rgba(31, 157, 85, .12); }
.kpi-delta.down { color: var(--sale); background: rgba(224, 36, 94, .12); }
.spark { width: 100%; height: 34px; align-self: end; overflow: visible; }
/* ---------- Panels ---------- */
.grid-2 {
display: grid;
grid-template-columns: 1.7fr 1fr;
gap: 16px;
}
.grid-2--wide { grid-template-columns: 1.55fr 1fr; }
.panel {
background: var(--surface);
border: 1px solid var(--line-soft);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
min-width: 0;
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.panel h2 { font-size: 1.02rem; }
.panel-sub { color: var(--muted); font-size: .8rem; margin-top: 2px; }
.pill {
font-size: .72rem;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
}
.pill-warn { background: rgba(217, 119, 6, .14); color: var(--warn); }
/* ---------- Revenue chart ---------- */
.chart-legend { display: flex; gap: 14px; flex-wrap: wrap; }
.lg-item { display: inline-flex; align-items: center; gap: 6px; font-size: .76rem; color: var(--muted); font-weight: 500; }
.lg-swatch { width: 12px; height: 12px; border-radius: 4px; }
.lg-rev { background: var(--brand); }
.lg-prev { background: repeating-linear-gradient(90deg, #c8cdda 0 4px, transparent 4px 8px); height: 3px; border-radius: 2px; }
.chart-wrap { position: relative; }
.rev-chart { width: 100%; height: auto; display: block; }
.rev-area { fill: url(#revGrad); }
.rev-line { fill: none; stroke: var(--brand); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.prev-line { fill: none; stroke: #c8cdda; stroke-width: 2; stroke-dasharray: 5 5; }
.grid-line { stroke: var(--line-soft); stroke-width: 1; }
.axis-label { fill: var(--muted); font-size: 11px; font-weight: 500; }
.hover-dot { fill: var(--brand); stroke: #fff; stroke-width: 2.5; pointer-events: none; }
.hover-rule { stroke: var(--line); stroke-width: 1; pointer-events: none; }
.hit { fill: transparent; cursor: crosshair; }
.chart-tip {
position: absolute;
pointer-events: none;
background: var(--ink);
color: #fff;
font-size: .76rem;
font-weight: 600;
padding: 7px 10px;
border-radius: 8px;
transform: translate(-50%, -120%);
white-space: nowrap;
box-shadow: 0 8px 20px rgba(0, 0, 0, .22);
z-index: 5;
}
.chart-tip b { display: block; font-size: .9rem; font-weight: 800; }
.chart-tip span { color: #aab1c4; font-weight: 500; }
/* ---------- Donut ---------- */
.donut-wrap { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.donut { width: 150px; height: 150px; flex: none; }
.donut-center-val { font-size: 19px; font-weight: 800; fill: var(--ink); }
.donut-center-lbl { font-size: 8.5px; font-weight: 600; fill: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.donut-seg { transition: opacity .15s; cursor: default; }
.donut-seg:hover { opacity: .82; }
.donut-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; flex: 1; min-width: 130px; }
.donut-legend li { display: flex; align-items: center; gap: 9px; font-size: .82rem; }
.dl-swatch { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.dl-name { color: var(--ink-soft); font-weight: 500; }
.dl-val { margin-left: auto; font-weight: 700; }
.dl-pct { color: var(--muted); font-size: .76rem; min-width: 34px; text-align: right; }
/* ---------- Table ---------- */
.table-scroll { overflow-x: auto; margin: 0 -4px; }
.data-table { width: 100%; border-collapse: collapse; font-size: .86rem; min-width: 460px; }
.data-table th {
text-align: left;
padding: 0 12px 10px;
border-bottom: 1px solid var(--line);
}
.data-table th.num, .data-table td.num { text-align: right; }
.th-sort {
border: 0;
background: none;
color: var(--muted);
font-size: .76rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .04em;
padding: 0;
display: inline-flex;
align-items: center;
gap: 4px;
}
.th-sort::after { content: "↕"; opacity: .35; font-size: .8em; }
.th-sort.is-sorted { color: var(--ink); }
.th-sort.is-sorted[data-dir="desc"]::after { content: "↓"; opacity: 1; color: var(--brand); }
.th-sort.is-sorted[data-dir="asc"]::after { content: "↑"; opacity: 1; color: var(--brand); }
.data-table td { padding: 11px 12px; border-bottom: 1px solid var(--line-soft); }
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr { transition: background .12s; }
.data-table tbody tr:hover { background: var(--brand-tint); }
.prod-cell { display: flex; align-items: center; gap: 11px; }
.prod-thumb {
width: 38px; height: 38px;
border-radius: 9px;
flex: none;
display: grid; place-items: center;
}
.prod-thumb svg { width: 22px; height: 22px; }
.prod-name { font-weight: 600; }
.prod-sku { color: var(--muted); font-size: .74rem; }
.rev-strong { font-weight: 700; }
.margin-chip {
font-size: .76rem;
font-weight: 600;
color: var(--ok);
}
.stock-chip {
font-size: .74rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
background: rgba(31, 157, 85, .12);
color: var(--ok);
white-space: nowrap;
}
.stock-chip.low { background: rgba(217, 119, 6, .14); color: var(--warn); }
.stock-chip.out { background: rgba(224, 36, 94, .12); color: var(--sale); }
/* ---------- Side column ---------- */
.side-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.alert-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.alert-item {
display: flex;
align-items: center;
gap: 11px;
padding: 9px;
border-radius: 11px;
transition: background .12s;
}
.alert-item:hover { background: var(--bg); }
.alert-thumb {
width: 34px; height: 34px; border-radius: 9px; flex: none;
display: grid; place-items: center;
}
.alert-thumb svg { width: 18px; height: 18px; }
.alert-body { min-width: 0; flex: 1; }
.alert-name { font-weight: 600; font-size: .85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.alert-meta { color: var(--muted); font-size: .76rem; }
.stock-bar {
height: 5px;
border-radius: 999px;
background: var(--line);
overflow: hidden;
margin-top: 5px;
}
.stock-bar > i { display: block; height: 100%; border-radius: 999px; }
.reorder-btn {
border: 1px solid var(--line);
background: var(--surface);
color: var(--brand);
font-size: .74rem;
font-weight: 700;
padding: 5px 11px;
border-radius: 8px;
flex: none;
transition: background .12s, border-color .12s;
}
.reorder-btn:hover { background: var(--brand-tint); border-color: var(--brand); }
.reorder-btn:disabled { color: var(--muted); background: var(--bg); cursor: default; border-color: var(--line-soft); }
/* ---------- Feed ---------- */
.feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.feed-item {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--line-soft);
}
.feed-item:last-child { border-bottom: 0; }
.feed-avatar {
width: 34px; height: 34px; border-radius: 50%;
display: grid; place-items: center;
color: #fff; font-size: .74rem; font-weight: 700;
flex: none;
}
.feed-body { min-width: 0; flex: 1; }
.feed-name { font-weight: 600; font-size: .86rem; }
.feed-sub { color: var(--muted); font-size: .77rem; }
.feed-amt { font-weight: 700; font-size: .88rem; }
.feed-status {
display: inline-block;
font-size: .68rem;
font-weight: 700;
padding: 1px 7px;
border-radius: 999px;
margin-top: 2px;
}
.st-paid { background: rgba(31, 157, 85, .12); color: var(--ok); }
.st-pending { background: rgba(217, 119, 6, .14); color: var(--warn); }
.st-shipped { background: var(--brand-tint); color: var(--brand-d); }
.st-refunded { background: rgba(224, 36, 94, .12); color: var(--sale); }
.feed-right { text-align: right; flex: none; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(12px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: 11px;
font-size: .86rem;
font-weight: 600;
box-shadow: 0 12px 30px rgba(0, 0, 0, .28);
opacity: 0;
transition: opacity .2s, transform .2s;
z-index: 100;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid-2, .grid-2--wide { grid-template-columns: 1fr; }
}
@media (max-width: 860px) {
:root { --sb: 0px; }
.app { grid-template-columns: 1fr; }
.sidebar {
position: fixed;
z-index: 60;
left: 0; top: 0;
width: 252px;
height: 100vh;
transform: translateX(-100%);
transition: transform .25s ease;
box-shadow: 0 0 40px rgba(0, 0, 0, .3);
}
.app.nav-open .sidebar { transform: translateX(0); }
.menu-btn { display: inline-grid; place-items: center; }
.scrim {
position: fixed; inset: 0; z-index: 55;
background: rgba(15, 19, 32, .45);
border: 0;
}
}
@media (max-width: 520px) {
.kpis { grid-template-columns: 1fr; }
.topbar { padding: 14px 16px; }
.topbar h1 { font-size: 1.15rem; }
.donut-wrap { justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}(function () {
"use strict";
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
const $ = (sel, ctx) => (ctx || document).querySelector(sel);
const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
const SVGNS = "http://www.w3.org/2000/svg";
const usd = (n) =>
"$" + Math.round(n).toLocaleString("en-US");
const usd2 = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function el(tag, attrs, txt) {
const e = document.createElementNS(SVGNS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (txt != null) e.textContent = txt;
return e;
}
// Deterministic pseudo-random generator so data is stable per period.
function rng(seed) {
let s = seed % 2147483647;
if (s <= 0) s += 2147483646;
return () => (s = (s * 16807) % 2147483647) / 2147483647;
}
let toastTimer;
function toast(msg) {
const t = $("#toast");
t.textContent = msg;
t.hidden = false;
requestAnimationFrame(() => t.classList.add("show"));
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
t.classList.remove("show");
setTimeout(() => (t.hidden = true), 220);
}, 2400);
}
/* ------------------------------------------------------------------ */
/* Data model */
/* ------------------------------------------------------------------ */
// Per-period config: days of series, multiplier, conversion baseline.
const PERIODS = {
7: { points: 7, unit: "day", label: "Last 7 days", conv: 3.4, scale: 1 },
30: { points: 30, unit: "day", label: "Last 30 days", conv: 3.1, scale: 1 },
90: { points: 13, unit: "week", label: "Last 90 days", conv: 2.8, scale: 7.1 },
365: { points: 12, unit: "month",label: "Last 12 months",conv: 2.9, scale: 30.5 }
};
// Generate a revenue series + total metrics for a given period.
function buildPeriod(days) {
const cfg = PERIODS[days];
const rand = rng(days * 131 + 7);
const base = 1850 + rand() * 600;
const series = [];
let trend = base;
for (let i = 0; i < cfg.points; i++) {
trend *= 0.985 + rand() * 0.045; // gentle organic walk
const weekend = cfg.unit === "day" && (i % 7 === 5 || i % 7 === 6);
const v = trend * cfg.scale * (weekend ? 1.28 : 1) * (0.85 + rand() * 0.3);
series.push(Math.max(180, v));
}
// previous-period comparison series (slightly lower on average)
const prev = series.map((v, i) => v * (0.82 + (rng(days * 31 + i)() * 0.22)));
const revenue = series.reduce((a, b) => a + b, 0);
const prevRevenue = prev.reduce((a, b) => a + b, 0);
const aov = 58 + rand() * 26 + days * 0.02;
const orders = Math.round(revenue / aov);
const prevOrders = Math.round(prevRevenue / (aov * (0.95 + rand() * 0.08)));
const conv = cfg.conv + (rand() - 0.5) * 0.6;
const prevConv = conv * (0.9 + rand() * 0.16);
return {
cfg, series, prev,
sales: { value: revenue, fmt: usd(revenue), delta: pct(revenue, prevRevenue) },
orders: { value: orders, fmt: orders.toLocaleString("en-US"), delta: pct(orders, prevOrders) },
aov: { value: aov, fmt: usd2(aov), delta: pct(aov, aov * (0.94 + rand() * 0.1)) },
conv: { value: conv, fmt: conv.toFixed(1) + "%", delta: pct(conv, prevConv) }
};
}
const pct = (a, b) => (b === 0 ? 0 : ((a - b) / b) * 100);
// Top products (revenue scales softly with period).
const PRODUCTS = [
{ name: "Aurora Wireless Earbuds", sku: "LG-AUR-021", units: 482, revenue: 38560, margin: 41, stock: 132, tint: "#eef1ff", ink: "#3457ff", icon: "earbuds" },
{ name: "Drift Merino Beanie", sku: "LG-DRF-118", units: 651, revenue: 19530, margin: 58, stock: 8, tint: "#fef0f4", ink: "#e0245e", icon: "beanie" },
{ name: "Lumen Desk Lamp", sku: "LG-LMP-007", units: 214, revenue: 17120, margin: 47, stock: 0, tint: "#fff4e6", ink: "#d97706", icon: "lamp" },
{ name: "Halo Ceramic Mug 12oz", sku: "LG-MUG-330", units: 903, revenue: 13545, margin: 63, stock: 47, tint: "#f0ecff", ink: "#8b5cf6", icon: "mug" },
{ name: "Trail Canvas Backpack", sku: "LG-BPK-205", units: 176, revenue: 12320, margin: 39, stock: 21, tint: "#e9faf0", ink: "#1f9d55", icon: "bag" },
{ name: "Pulse Smart Bottle 750ml", sku: "LG-BTL-094", units: 388, revenue: 11640, margin: 52, stock: 4, tint: "#eef1ff", ink: "#3457ff", icon: "bottle" },
{ name: "Nimbus Throw Blanket", sku: "LG-BLK-411", units: 142, revenue: 9940, margin: 44, stock: 63, tint: "#fef0f4", ink: "#e0245e", icon: "blanket" }
];
const ICONS = {
earbuds: '<path d="M8 4a4 4 0 00-4 4v6a3 3 0 003 3 2 2 0 002-2v-7a4 4 0 014-4M16 4a4 4 0 014 4v6a3 3 0 01-3 3 2 2 0 01-2-2v-7a4 4 0 00-4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>',
beanie: '<path d="M4 16a8 8 0 0116 0M3 16h18v3H3z" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
lamp: '<path d="M9 3h6l3 7H6l3-7zM12 10v8M8 21h8" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
mug: '<path d="M5 6h11v9a4 4 0 01-4 4H9a4 4 0 01-4-4V6zM16 8h2a2 2 0 012 2v1a2 2 0 01-2 2h-2" stroke="currentColor" stroke-width="2" fill="none" stroke-linejoin="round"/>',
bag: '<path d="M6 8h12l1 12H5L6 8zM9 8V6a3 3 0 016 0v2" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
bottle: '<path d="M10 2h4v3l1.5 2.5V20a2 2 0 01-2 2h-3a2 2 0 01-2-2V7.5L10 5V2zM8.5 11h7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
blanket: '<path d="M4 6h16v9a3 3 0 01-3 3H4V6zM4 18l3-3M20 6v9" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>'
};
const STATUS = [
{ key: "Paid", value: 0, color: "var(--c-2)" },
{ key: "Shipped", value: 0, color: "var(--c-1)" },
{ key: "Pending", value: 0, color: "var(--c-3)" },
{ key: "Refunded", value: 0, color: "var(--c-5)" }
];
const FEED = [
{ name: "Priya Nair", sku: "#LG-9043", items: "Aurora Earbuds +1", amt: 89.0, status: "paid", color: "#3457ff" },
{ name: "Marcus Webb", sku: "#LG-9042", items: "Lumen Desk Lamp", amt: 64.0, status: "shipped", color: "#1f9d55" },
{ name: "Hana Sato", sku: "#LG-9041", items: "Halo Mug ×3", amt: 45.0, status: "pending", color: "#d97706" },
{ name: "Diego Alvarez", sku: "#LG-9040", items: "Trail Backpack", amt: 70.0, status: "paid", color: "#8b5cf6" },
{ name: "Lena Fischer", sku: "#LG-9039", items: "Drift Beanie ×2", amt: 60.0, status: "refunded", color: "#e0245e" },
{ name: "Omar Haddad", sku: "#LG-9038", items: "Pulse Bottle", amt: 30.0, status: "paid", color: "#3457ff" }
];
/* ------------------------------------------------------------------ */
/* KPI cards + sparklines */
/* ------------------------------------------------------------------ */
function sparkPoints(seed, up) {
const rand = rng(seed);
const vals = [];
let v = 0.5;
for (let i = 0; i < 14; i++) {
v += (rand() - (up ? 0.42 : 0.58)) * 0.22;
v = Math.max(0.08, Math.min(0.92, v));
vals.push(v);
}
return vals;
}
function drawSpark(svg, seed, up) {
while (svg.firstChild) svg.removeChild(svg.firstChild);
const W = 120, H = 36, vals = sparkPoints(seed, up);
const stepX = W / (vals.length - 1);
const pts = vals.map((v, i) => [i * stepX, H - v * (H - 6) - 3]);
const line = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" ");
const area = line + ` L${W} ${H} L0 ${H} Z`;
const color = up ? "var(--ok)" : "var(--sale)";
const gid = "sg" + seed;
const defs = el("defs");
const grad = el("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1" });
grad.appendChild(el("stop", { offset: "0", "stop-color": color, "stop-opacity": ".28" }));
grad.appendChild(el("stop", { offset: "1", "stop-color": color, "stop-opacity": "0" }));
defs.appendChild(grad);
svg.appendChild(defs);
svg.appendChild(el("path", { d: area, fill: `url(#${gid})` }));
svg.appendChild(el("path", { d: line, fill: "none", stroke: color, "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round" }));
const last = pts[pts.length - 1];
svg.appendChild(el("circle", { cx: last[0], cy: last[1], r: "2.4", fill: color }));
}
function renderKPIs(data) {
const map = { sales: data.sales, orders: data.orders, aov: data.aov, conv: data.conv };
$$(".kpi").forEach((card) => {
const key = card.dataset.kpi;
const m = map[key];
$("[data-value]", card).textContent = m.fmt;
const d = $("[data-delta]", card);
const up = m.delta >= 0;
d.className = "kpi-delta " + (up ? "up" : "down");
d.textContent = (up ? "▲ " : "▼ ") + Math.abs(m.delta).toFixed(1) + "%";
drawSpark($(".spark", card), (key.charCodeAt(0) + key.length) * 17 + Object.keys(map).indexOf(key) * 5, up);
});
}
/* ------------------------------------------------------------------ */
/* Revenue line/area chart */
/* ------------------------------------------------------------------ */
const revChart = $("#revChart");
const chartTip = $("#chartTip");
let chartState = null; // { xs, ys, series, labels }
function fmtAxis(i, cfg) {
if (cfg.unit === "month") return ["Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr","May","Jun"][i] || "";
if (cfg.unit === "week") return "W" + (i + 1);
return "D" + (i + 1);
}
function drawRevenue(data) {
const svg = revChart;
while (svg.firstChild) svg.removeChild(svg.firstChild);
const W = 720, H = 260, padL = 46, padR = 14, padT = 14, padB = 28;
const series = data.series, prev = data.prev;
const max = Math.max(...series, ...prev) * 1.08;
const min = 0;
const plotW = W - padL - padR, plotH = H - padT - padB;
const xAt = (i) => padL + (i / (series.length - 1)) * plotW;
const yAt = (v) => padT + plotH - ((v - min) / (max - min)) * plotH;
// gradient defs
const defs = el("defs");
const grad = el("linearGradient", { id: "revGrad", x1: "0", y1: "0", x2: "0", y2: "1" });
grad.appendChild(el("stop", { offset: "0", "stop-color": "var(--brand)", "stop-opacity": ".22" }));
grad.appendChild(el("stop", { offset: "1", "stop-color": "var(--brand)", "stop-opacity": "0" }));
defs.appendChild(grad);
svg.appendChild(defs);
// y grid + labels
const ticks = 4;
for (let t = 0; t <= ticks; t++) {
const v = (max / ticks) * t;
const y = yAt(v);
svg.appendChild(el("line", { class: "grid-line", x1: padL, y1: y, x2: W - padR, y2: y }));
const lbl = el("text", { class: "axis-label", x: padL - 8, y: y + 4, "text-anchor": "end" }, "$" + Math.round(v / 1000) + "k");
svg.appendChild(lbl);
}
// x labels (max ~7 spread)
const xs = series.map((_, i) => xAt(i));
const ys = series.map((v) => yAt(v));
const labelEvery = Math.ceil(series.length / 7);
series.forEach((_, i) => {
if (i % labelEvery === 0 || i === series.length - 1) {
svg.appendChild(el("text", { class: "axis-label", x: xAt(i), y: H - 8, "text-anchor": "middle" }, fmtAxis(i, data.cfg)));
}
});
// prev line (dashed)
const prevPath = prev.map((v, i) => (i ? "L" : "M") + xAt(i).toFixed(1) + " " + yAt(v).toFixed(1)).join(" ");
svg.appendChild(el("path", { class: "prev-line", d: prevPath }));
// area + line
const linePath = series.map((v, i) => (i ? "L" : "M") + xs[i].toFixed(1) + " " + ys[i].toFixed(1)).join(" ");
const areaPath = linePath + ` L${xs[xs.length - 1]} ${padT + plotH} L${xs[0]} ${padT + plotH} Z`;
const area = el("path", { class: "rev-area", d: areaPath });
svg.appendChild(area);
const line = el("path", { class: "rev-line", d: linePath });
svg.appendChild(line);
// animate line draw
const len = line.getTotalLength ? line.getTotalLength() : 0;
if (len) {
line.style.strokeDasharray = len;
line.style.strokeDashoffset = len;
requestAnimationFrame(() => {
line.style.transition = "stroke-dashoffset .7s ease";
line.style.strokeDashoffset = "0";
});
}
// hover layer
const rule = el("line", { class: "hover-rule", x1: 0, y1: padT, x2: 0, y2: padT + plotH, opacity: "0" });
const dot = el("circle", { class: "hover-dot", r: "4.5", cx: 0, cy: 0, opacity: "0" });
svg.appendChild(rule);
svg.appendChild(dot);
const hit = el("rect", { class: "hit", x: padL, y: padT, width: plotW, height: plotH });
svg.appendChild(hit);
chartState = { xs, ys, series, cfg: data.cfg, padL, plotW, rule, dot, W, H };
hit.addEventListener("pointermove", onHover);
hit.addEventListener("pointerleave", () => {
rule.setAttribute("opacity", "0");
dot.setAttribute("opacity", "0");
chartTip.hidden = true;
});
}
function onHover(e) {
const st = chartState;
if (!st) return;
const rect = revChart.getBoundingClientRect();
const relX = ((e.clientX - rect.left) / rect.width) * st.W;
// nearest index
let i = Math.round(((relX - st.padL) / st.plotW) * (st.series.length - 1));
i = Math.max(0, Math.min(st.series.length - 1, i));
const x = st.xs[i], y = st.ys[i];
st.rule.setAttribute("x1", x); st.rule.setAttribute("x2", x); st.rule.setAttribute("opacity", "1");
st.dot.setAttribute("cx", x); st.dot.setAttribute("cy", y); st.dot.setAttribute("opacity", "1");
chartTip.hidden = false;
chartTip.innerHTML = `<b>${usd(st.series[i])}</b><span>${fmtAxis(i, st.cfg)}</span>`;
const px = (x / st.W) * rect.width;
const py = (y / st.H) * rect.height;
chartTip.style.left = px + "px";
chartTip.style.top = py + "px";
}
/* ------------------------------------------------------------------ */
/* Donut — orders by status */
/* ------------------------------------------------------------------ */
function drawDonut(totalOrders) {
const rand = rng(totalOrders + 3);
// distribute total across statuses
const weights = [0.62, 0.21, 0.12, 0.05].map((w) => w * (0.85 + rand() * 0.3));
const sum = weights.reduce((a, b) => a + b, 0);
STATUS.forEach((s, i) => (s.value = Math.round((weights[i] / sum) * totalOrders)));
// fix rounding so it sums to total
const diff = totalOrders - STATUS.reduce((a, b) => a + b.value, 0);
STATUS[0].value += diff;
const svg = $("#donut");
while (svg.firstChild) svg.removeChild(svg.firstChild);
const cx = 60, cy = 60, r = 44, sw = 18, C = 2 * Math.PI * r;
let offset = 0;
const total = STATUS.reduce((a, b) => a + b.value, 0) || 1;
// track
svg.appendChild(el("circle", { cx, cy, r, fill: "none", stroke: "var(--line-soft)", "stroke-width": sw }));
STATUS.forEach((s) => {
const frac = s.value / total;
const seg = el("circle", {
class: "donut-seg",
cx, cy, r,
fill: "none",
stroke: s.color,
"stroke-width": sw,
"stroke-dasharray": `${(frac * C).toFixed(2)} ${C.toFixed(2)}`,
"stroke-dashoffset": (-offset * C).toFixed(2),
transform: `rotate(-90 ${cx} ${cy})`,
"stroke-linecap": "butt"
});
const t = el("title", {}, `${s.key}: ${s.value} (${(frac * 100).toFixed(0)}%)`);
seg.appendChild(t);
svg.appendChild(seg);
offset += frac;
});
svg.appendChild(el("text", { class: "donut-center-val", x: cx, y: cy - 2, "text-anchor": "middle" }, total.toLocaleString("en-US")));
svg.appendChild(el("text", { class: "donut-center-lbl", x: cx, y: cy + 12, "text-anchor": "middle" }, "Orders"));
// legend
const lg = $("#donutLegend");
lg.innerHTML = "";
STATUS.forEach((s) => {
const li = document.createElement("li");
li.innerHTML =
`<span class="dl-swatch" style="background:${s.color}"></span>` +
`<span class="dl-name">${s.key}</span>` +
`<span class="dl-val">${s.value.toLocaleString("en-US")}</span>` +
`<span class="dl-pct">${((s.value / total) * 100).toFixed(0)}%</span>`;
lg.appendChild(li);
});
$("#donutTotal").textContent = total.toLocaleString("en-US") + " orders";
}
/* ------------------------------------------------------------------ */
/* Top products table + sorting */
/* ------------------------------------------------------------------ */
let sortKey = "revenue", sortDir = "desc", periodScale = 1;
function stockClass(n) { return n === 0 ? "out" : n <= 12 ? "low" : ""; }
function stockLabel(n) { return n === 0 ? "Out of stock" : n + " left"; }
function renderTable() {
const body = $("#productsBody");
body.innerHTML = "";
const rows = PRODUCTS.map((p) => ({
...p,
units: Math.round(p.units * periodScale),
revenue: Math.round(p.revenue * periodScale)
}));
rows.sort((a, b) => {
let av = a[sortKey], bv = b[sortKey];
if (typeof av === "string") { av = av.toLowerCase(); bv = bv.toLowerCase(); }
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
return sortDir === "asc" ? cmp : -cmp;
});
rows.forEach((p) => {
const tr = document.createElement("tr");
tr.innerHTML =
`<td><div class="prod-cell">
<span class="prod-thumb" style="background:${p.tint};color:${p.ink}">
<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS[p.icon]}</svg>
</span>
<span><span class="prod-name">${p.name}</span><br><span class="prod-sku">${p.sku}</span></span>
</div></td>` +
`<td class="num">${p.units.toLocaleString("en-US")}</td>` +
`<td class="num rev-strong">${usd(p.revenue)}</td>` +
`<td class="num"><span class="margin-chip">${p.margin}%</span></td>` +
`<td class="num"><span class="stock-chip ${stockClass(p.stock)}">${stockLabel(p.stock)}</span></td>`;
body.appendChild(tr);
});
}
function wireSort() {
$$(".th-sort").forEach((btn) => {
if (!btn.dataset.sort) return;
btn.addEventListener("click", () => {
const key = btn.dataset.sort;
if (key === sortKey) {
sortDir = sortDir === "asc" ? "desc" : "asc";
} else {
sortKey = key;
sortDir = key === "name" ? "asc" : "desc";
}
$$(".th-sort").forEach((b) => { b.classList.remove("is-sorted"); b.removeAttribute("data-dir"); });
btn.classList.add("is-sorted");
btn.setAttribute("data-dir", sortDir);
renderTable();
});
});
}
/* ------------------------------------------------------------------ */
/* Low stock list */
/* ------------------------------------------------------------------ */
function renderLowStock() {
const list = $("#lowStock");
list.innerHTML = "";
const low = PRODUCTS.filter((p) => p.stock <= 12).sort((a, b) => a.stock - b.stock);
$("#lowCount").textContent = low.length;
low.forEach((p) => {
const li = document.createElement("li");
li.className = "alert-item";
const ratio = Math.max(0.04, Math.min(1, p.stock / 40));
const barColor = p.stock === 0 ? "var(--sale)" : "var(--warn)";
li.innerHTML =
`<span class="alert-thumb" style="background:${p.tint};color:${p.ink}">
<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS[p.icon]}</svg>
</span>
<div class="alert-body">
<div class="alert-name">${p.name}</div>
<div class="alert-meta">${p.sku} · ${stockLabel(p.stock)}</div>
<div class="stock-bar"><i style="width:${(ratio * 100).toFixed(0)}%;background:${barColor}"></i></div>
</div>
<button class="reorder-btn" type="button">Reorder</button>`;
const btn = $(".reorder-btn", li);
btn.addEventListener("click", () => {
btn.disabled = true;
btn.textContent = "Queued";
toast("Reorder queued for " + p.name);
});
list.appendChild(li);
});
}
/* ------------------------------------------------------------------ */
/* Recent orders feed */
/* ------------------------------------------------------------------ */
function renderFeed() {
const ul = $("#feed");
ul.innerHTML = "";
const labels = { paid: "Paid", pending: "Pending", shipped: "Shipped", refunded: "Refunded" };
FEED.forEach((o, idx) => {
const li = document.createElement("li");
li.className = "feed-item";
const initials = o.name.split(" ").map((s) => s[0]).join("").slice(0, 2);
li.innerHTML =
`<span class="feed-avatar" style="background:${o.color}">${initials}</span>
<div class="feed-body">
<div class="feed-name">${o.name}</div>
<div class="feed-sub">${o.sku} · ${o.items}</div>
</div>
<div class="feed-right">
<div class="feed-amt">${usd2(o.amt)}</div>
<span class="feed-status st-${o.status}">${labels[o.status]}</span>
</div>`;
ul.appendChild(li);
});
}
/* ------------------------------------------------------------------ */
/* Period switching */
/* ------------------------------------------------------------------ */
function setPeriod(days) {
const data = buildPeriod(days);
periodScale = days === 7 ? 0.32 : days === 30 ? 1 : days === 90 ? 2.7 : 11;
renderKPIs(data);
drawRevenue(data);
drawDonut(data.orders.value);
renderTable();
renderLowStock();
renderFeed();
$("#revRange").textContent = data.cfg.label;
}
function wirePeriod() {
$$(".period-btn").forEach((btn) => {
btn.addEventListener("click", () => {
$$(".period-btn").forEach((b) => { b.classList.remove("is-active"); b.setAttribute("aria-selected", "false"); });
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
setPeriod(Number(btn.dataset.period));
toast("Showing " + PERIODS[Number(btn.dataset.period)].label.toLowerCase());
});
});
}
/* ------------------------------------------------------------------ */
/* Sidebar toggle (mobile) + export */
/* ------------------------------------------------------------------ */
function wireChrome() {
const app = $(".app");
const menuBtn = $("#menuBtn");
let scrim = null;
menuBtn.addEventListener("click", () => {
const open = app.classList.toggle("nav-open");
menuBtn.setAttribute("aria-expanded", String(open));
if (open) {
scrim = document.createElement("button");
scrim.className = "scrim";
scrim.setAttribute("aria-label", "Close navigation");
scrim.addEventListener("click", closeNav);
app.appendChild(scrim);
} else {
closeNav();
}
});
function closeNav() {
app.classList.remove("nav-open");
menuBtn.setAttribute("aria-expanded", "false");
if (scrim) { scrim.remove(); scrim = null; }
}
$$(".nav-link").forEach((l) =>
l.addEventListener("click", (e) => {
e.preventDefault();
$$(".nav-link").forEach((n) => { n.classList.remove("is-active"); n.removeAttribute("aria-current"); });
l.classList.add("is-active");
l.setAttribute("aria-current", "page");
if (window.innerWidth <= 860) closeNav();
})
);
$("#exportBtn").addEventListener("click", () => toast("Report exported to CSV"));
}
// redraw chart on resize so the hit-area math stays correct
let rt;
window.addEventListener("resize", () => {
clearTimeout(rt);
rt = setTimeout(() => {
const active = $(".period-btn.is-active");
if (active) {
const data = buildPeriod(Number(active.dataset.period));
drawRevenue(data);
}
}, 180);
});
/* ------------------------------------------------------------------ */
/* Init */
/* ------------------------------------------------------------------ */
wireSort();
wirePeriod();
wireChrome();
setPeriod(30);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumen Goods — Store 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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
<path d="M4 8l8-4 8 4-8 4-8-4z" fill="currentColor" opacity=".9"/>
<path d="M4 8v8l8 4V12L4 8z" fill="currentColor" opacity=".55"/>
<path d="M20 8v8l-8 4V12l8-4z" fill="currentColor" opacity=".75"/>
</svg>
</span>
<span class="brand-name">Lumen<strong>Goods</strong></span>
</div>
<nav class="nav" aria-label="Dashboard sections">
<a href="#" class="nav-link is-active" aria-current="page">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/></svg>
Overview
</a>
<a href="#" class="nav-link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 4h-.5L5 2H2v2h2l3.6 7.6-1.3 2.5c-.6 1.2.2 2.9 1.7 2.9h11v-2H8l1-2h7.5c.8 0 1.4-.4 1.7-1l3.6-6.6L18 4H7zm0 16a2 2 0 100 4 2 2 0 000-4zm10 0a2 2 0 100 4 2 2 0 000-4z" fill="currentColor"/></svg>
Orders <span class="nav-badge">28</span>
</a>
<a href="#" class="nav-link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 2l9 4v6c0 5-3.8 9.3-9 10-5.2-.7-9-5-9-10V6l9-4z" fill="currentColor"/></svg>
Products
</a>
<a href="#" class="nav-link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M16 11a4 4 0 10-4-4 4 4 0 004 4zm-8 0a4 4 0 10-4-4 4 4 0 004 4zm0 2c-2.7 0-8 1.3-8 4v3h9v-3c0-1 .4-1.8 1-2.5C8.6 13.2 8.3 13 8 13zm8 0c-.3 0-.7 0-1.1.1.7.7 1.1 1.6 1.1 2.6V20h8v-3c0-2.7-5.3-4-8-4z" fill="currentColor"/></svg>
Customers
</a>
<a href="#" class="nav-link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M5 9.2h3v9.6H5zM10.6 5h3v13.8h-3zM16.2 13h3v5.8h-3z" fill="currentColor"/></svg>
Analytics
</a>
<a href="#" class="nav-link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M19.4 13a7.8 7.8 0 000-2l2.1-1.6-2-3.5-2.5 1a8 8 0 00-1.7-1l-.4-2.7h-4l-.4 2.7a8 8 0 00-1.7 1l-2.5-1-2 3.5L4.6 11a7.8 7.8 0 000 2l-2.1 1.6 2 3.5 2.5-1a8 8 0 001.7 1l.4 2.7h4l.4-2.7a8 8 0 001.7-1l2.5 1 2-3.5L19.4 13zM12 15.5a3.5 3.5 0 110-7 3.5 3.5 0 010 7z" fill="currentColor"/></svg>
Settings
</a>
</nav>
<div class="store-card">
<div class="store-avatar" aria-hidden="true">LG</div>
<div class="store-meta">
<span class="store-name">Lumen Goods</span>
<span class="store-plan">Pro plan · Online</span>
</div>
<span class="dot-live" aria-hidden="true"></span>
</div>
</aside>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="topbar-head">
<button class="menu-btn" id="menuBtn" aria-label="Toggle navigation" aria-expanded="false">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<div>
<h1>Store dashboard</h1>
<p class="topbar-sub">Good evening, Mara — here's how Lumen Goods is performing.</p>
</div>
</div>
<div class="topbar-tools">
<div class="period" role="tablist" aria-label="Reporting period">
<button role="tab" class="period-btn" data-period="7" aria-selected="false">7d</button>
<button role="tab" class="period-btn is-active" data-period="30" aria-selected="true">30d</button>
<button role="tab" class="period-btn" data-period="90" aria-selected="false">90d</button>
<button role="tab" class="period-btn" data-period="365" aria-selected="false">12m</button>
</div>
<button class="ghost-btn" id="exportBtn">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 3v10m0 0l-4-4m4 4l4-4M5 17v2a2 2 0 002 2h10a2 2 0 002-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
Export
</button>
</div>
</header>
<main class="content" aria-label="Store metrics">
<!-- KPI cards -->
<section class="kpis" aria-label="Key performance indicators">
<article class="kpi" data-kpi="sales">
<header class="kpi-top">
<span class="kpi-label">Net sales</span>
<span class="kpi-delta" data-delta></span>
</header>
<strong class="kpi-value" data-value>$0</strong>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"></svg>
</article>
<article class="kpi" data-kpi="orders">
<header class="kpi-top">
<span class="kpi-label">Orders</span>
<span class="kpi-delta" data-delta></span>
</header>
<strong class="kpi-value" data-value>0</strong>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"></svg>
</article>
<article class="kpi" data-kpi="aov">
<header class="kpi-top">
<span class="kpi-label">Avg. order value</span>
<span class="kpi-delta" data-delta></span>
</header>
<strong class="kpi-value" data-value>$0</strong>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"></svg>
</article>
<article class="kpi" data-kpi="conv">
<header class="kpi-top">
<span class="kpi-label">Conversion</span>
<span class="kpi-delta" data-delta></span>
</header>
<strong class="kpi-value" data-value>0%</strong>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"></svg>
</article>
</section>
<!-- Revenue chart + donut -->
<section class="grid-2">
<article class="panel chart-panel">
<header class="panel-head">
<div>
<h2>Revenue</h2>
<p class="panel-sub" id="revRange">Last 30 days</p>
</div>
<div class="chart-legend">
<span class="lg-item"><i class="lg-swatch lg-rev"></i>Revenue</span>
<span class="lg-item"><i class="lg-swatch lg-prev"></i>Prev. period</span>
</div>
</header>
<div class="chart-wrap">
<svg class="rev-chart" id="revChart" viewBox="0 0 720 260" preserveAspectRatio="none" role="img" aria-label="Revenue over time line chart"></svg>
<div class="chart-tip" id="chartTip" hidden role="status" aria-live="polite"></div>
</div>
</article>
<article class="panel donut-panel">
<header class="panel-head">
<div>
<h2>Orders by status</h2>
<p class="panel-sub" id="donutTotal">0 orders</p>
</div>
</header>
<div class="donut-wrap">
<svg class="donut" id="donut" viewBox="0 0 120 120" role="img" aria-label="Orders by status donut chart"></svg>
<ul class="donut-legend" id="donutLegend"></ul>
</div>
</article>
</section>
<!-- Top products + side column -->
<section class="grid-2 grid-2--wide">
<article class="panel">
<header class="panel-head">
<div>
<h2>Top products</h2>
<p class="panel-sub">By revenue this period</p>
</div>
</header>
<div class="table-scroll">
<table class="data-table" id="productsTable">
<thead>
<tr>
<th><button class="th-sort" data-sort="name">Product</button></th>
<th class="num"><button class="th-sort" data-sort="units">Units</button></th>
<th class="num"><button class="th-sort is-sorted" data-sort="revenue" data-dir="desc">Revenue</button></th>
<th class="num"><button class="th-sort" data-sort="margin">Margin</button></th>
<th class="num">Stock</th>
</tr>
</thead>
<tbody id="productsBody"></tbody>
</table>
</div>
</article>
<div class="side-col">
<article class="panel alert-panel">
<header class="panel-head">
<div>
<h2>Low stock</h2>
<p class="panel-sub">Reorder soon</p>
</div>
<span class="pill pill-warn" id="lowCount">0</span>
</header>
<ul class="alert-list" id="lowStock"></ul>
</article>
<article class="panel feed-panel">
<header class="panel-head">
<div>
<h2>Recent orders</h2>
<p class="panel-sub">Live feed</p>
</div>
</header>
<ul class="feed" id="feed"></ul>
</article>
</div>
</section>
</main>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Store Dashboard
A merchant control center for the fictional Lumen Goods store. A dark admin sidebar carries the brand, primary navigation and a store-status card, while the main column opens with four KPI cards — net sales, orders, average order value and conversion — each pairing a big value with a colored up/down delta chip and a tiny inline-SVG sparkline. Below sit the two hero charts: an animated revenue area/line chart with a dashed previous-period overlay, and an orders-by-status donut with a percentage legend. A sortable top-products table, a low-stock reorder list and a live recent-orders feed fill out the grid.
Everything is driven by the segmented 7d / 30d / 90d / 12m period switch in the top bar. Picking a period deterministically rebuilds the revenue series and recomputes all four KPIs, their deltas and sparklines, redraws the revenue chart (with a stroke-draw animation) and the donut, and rescales the product table and feed. The revenue chart tracks the pointer with a crosshair rule, a marker dot and a tooltip showing the exact figure for the nearest day, week or month. Table headers sort by product, units, revenue or margin and toggle ascending/descending, and each low-stock row has a working Reorder button that confirms with a toast.
The layout is a CSS grid that collapses from four KPI columns to two to one and stacks the chart panels as the viewport narrows; below ~860px the sidebar becomes an off-canvas drawer behind a scrim, toggled by a hamburger button. All charts are hand-built inline SVG — no libraries, no images, no external requests beyond the Google Fonts link — and the whole screen reflows cleanly down to about 360px with visible focus rings on every control.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.