Salon — Retail Inventory
A back-office retail inventory screen for a boutique salon. Four summary cards track active SKUs, low and out-of-stock counts and total retail value, while a sortable table lists every shampoo, color tube and styling tool with brand, SKU, par level, supplier and shelf price. Inline plus-minus steppers adjust on-hand counts and recompute each status pill and row highlight live, filter tabs and a search box narrow the shelf, and a Reorder action fires on anything running low.
MCP
Code
:root {
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--gold: #b08d57;
--gold-d: #8c6d3f;
--gold-soft: #efe2cf;
--rose: #c9a78f;
--rose-soft: #f3e6dc;
--ink: #1c1814;
--ink-2: #3d362f;
--muted: #8a7d70;
--cream: #f7f1e8;
--bg: #faf6ef;
--white: #ffffff;
--line: rgba(28, 24, 20, 0.1);
--line-2: rgba(28, 24, 20, 0.18);
--ok: #5f8a6b;
--warn: #c08a3e;
--danger: #b3503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(28, 24, 20, 0.05);
--sh-md: 0 8px 24px -12px rgba(28, 24, 20, 0.22);
--sh-lg: 0 24px 60px -28px rgba(28, 24, 20, 0.34);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 520px at 85% -10%, var(--rose-soft), transparent 60%),
radial-gradient(900px 480px at -5% 0%, var(--gold-soft), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.inv {
max-width: 1120px;
margin: 0 auto;
padding: clamp(20px, 4vw, 48px) clamp(16px, 4vw, 40px) 56px;
}
/* ============ HEADER ============ */
.inv__head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.kicker {
margin: 0 0 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--gold-d);
}
.inv__title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: clamp(30px, 5.4vw, 46px);
line-height: 1.04;
letter-spacing: 0.01em;
color: var(--ink);
}
.inv__sub {
margin: 6px 0 0;
font-size: 13.5px;
color: var(--muted);
}
.inv__meta {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
font-size: 12.5px;
font-weight: 500;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-sm);
}
.chip__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px rgba(95, 138, 107, 0.18);
}
/* ============ BUTTONS ============ */
.btn {
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
padding: 9px 16px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.14s ease, box-shadow 0.18s ease,
background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.btn--ghost {
background: var(--white);
color: var(--ink-2);
border-color: var(--line-2);
box-shadow: var(--sh-sm);
}
.btn--ghost:hover {
border-color: var(--gold);
color: var(--gold-d);
}
/* ============ SUMMARY CARDS ============ */
.cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 26px 0 30px;
}
.card {
position: relative;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px 20px 16px;
box-shadow: var(--sh-md);
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--gold);
opacity: 0.7;
}
.card--low::before {
background: var(--warn);
}
.card--out::before {
background: var(--danger);
}
.card--value::before {
background: var(--rose);
}
.card__label {
margin: 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.card__value {
margin: 8px 0 2px;
font-family: var(--serif);
font-weight: 600;
font-size: 34px;
line-height: 1;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.card--low .card__value {
color: var(--warn);
}
.card--out .card__value {
color: var(--danger);
}
.card__note {
margin: 6px 0 0;
font-size: 12px;
color: var(--muted);
}
/* ============ TOOLBAR ============ */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.tabs {
display: inline-flex;
gap: 4px;
padding: 4px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-sm);
}
.tab {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: none;
padding: 8px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.tab:hover {
color: var(--gold-d);
}
.tab.is-active {
background: var(--ink);
color: var(--cream);
box-shadow: var(--sh-md);
}
.tab:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.tab__count {
font-size: 11.5px;
font-weight: 600;
min-width: 20px;
padding: 1px 7px;
border-radius: 999px;
background: var(--gold-soft);
color: var(--gold-d);
font-variant-numeric: tabular-nums;
}
.tab.is-active .tab__count {
background: rgba(247, 241, 232, 0.16);
color: var(--cream);
}
.search {
position: relative;
flex: 1;
min-width: 230px;
max-width: 360px;
}
.search__icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
pointer-events: none;
}
.search__input {
width: 100%;
font-family: var(--sans);
font-size: 13.5px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 11px 16px 11px 38px;
box-shadow: var(--sh-sm);
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.search__input::placeholder {
color: var(--muted);
}
.search__input:focus {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.16);
}
/* ============ PANEL + TABLE ============ */
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
overflow: hidden;
}
.table-wrap {
overflow-x: auto;
}
.table-wrap:focus-visible {
outline: 2px solid var(--gold);
outline-offset: -2px;
}
.tbl {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
.tbl thead th {
text-align: left;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
padding: 16px 16px 12px;
background: var(--cream);
border-bottom: 1px solid var(--line-2);
white-space: nowrap;
}
.tbl__num {
text-align: right;
}
.tbl__act {
text-align: right;
}
.tbl tbody td {
padding: 14px 16px;
border-bottom: 1px solid var(--line);
font-size: 13.5px;
color: var(--ink-2);
vertical-align: middle;
}
.tbl tbody tr {
transition: background 0.18s ease;
}
.tbl tbody tr:last-child td {
border-bottom: none;
}
.tbl tbody tr:hover {
background: rgba(176, 141, 87, 0.05);
}
.tbl tbody tr.row--low {
background: rgba(192, 138, 62, 0.07);
}
.tbl tbody tr.row--low:hover {
background: rgba(192, 138, 62, 0.12);
}
.tbl tbody tr.row--out {
background: rgba(179, 80, 62, 0.07);
}
.tbl tbody tr.row--out:hover {
background: rgba(179, 80, 62, 0.12);
}
.tbl tbody tr.flash {
animation: flash 0.6s ease;
}
@keyframes flash {
0% {
background: rgba(176, 141, 87, 0.28);
}
100% {
background: transparent;
}
}
/* product cell */
.prod {
display: flex;
align-items: center;
gap: 12px;
}
.prod__sw {
flex: none;
width: 38px;
height: 38px;
border-radius: var(--r-sm);
display: grid;
place-items: center;
font-family: var(--serif);
font-weight: 700;
font-size: 15px;
color: var(--white);
letter-spacing: 0.02em;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.prod__name {
font-weight: 600;
color: var(--ink);
font-size: 13.5px;
line-height: 1.2;
}
.prod__brand {
margin-top: 2px;
font-size: 11.5px;
color: var(--muted);
}
.sku {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
letter-spacing: 0.02em;
color: var(--muted);
white-space: nowrap;
}
.num {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 500;
white-space: nowrap;
}
.retail {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--ink);
white-space: nowrap;
}
.supplier {
font-size: 12.5px;
color: var(--muted);
white-space: nowrap;
}
/* stepper */
.stepper {
display: inline-flex;
align-items: center;
gap: 2px;
background: var(--cream);
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px;
}
.step {
width: 26px;
height: 26px;
border-radius: 50%;
border: none;
background: transparent;
color: var(--ink-2);
font-size: 16px;
line-height: 1;
font-weight: 600;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s ease, color 0.15s ease;
}
.step:hover:not(:disabled) {
background: var(--white);
color: var(--gold-d);
box-shadow: var(--sh-sm);
}
.step:active:not(:disabled) {
transform: scale(0.92);
}
.step:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 1px;
}
.step:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.stepper__val {
min-width: 30px;
text-align: center;
font-size: 13.5px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--ink);
}
/* status pill */
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 4px 11px;
border-radius: 999px;
white-space: nowrap;
}
.pill::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.pill--ok {
color: var(--ok);
background: rgba(95, 138, 107, 0.12);
}
.pill--low {
color: var(--warn);
background: rgba(192, 138, 62, 0.14);
}
.pill--out {
color: var(--danger);
background: rgba(179, 80, 62, 0.13);
}
/* reorder action */
.reorder {
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
color: var(--gold-d);
background: var(--white);
border: 1px solid var(--gold);
border-radius: 999px;
padding: 6px 13px;
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease, transform 0.12s ease;
white-space: nowrap;
}
.reorder:hover {
background: var(--gold);
color: var(--white);
}
.reorder:active {
transform: translateY(1px);
}
.reorder:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.reorder.is-done {
border-color: var(--line-2);
color: var(--muted);
cursor: default;
}
.reorder.is-done:hover {
background: var(--white);
color: var(--muted);
}
.act-cell {
text-align: right;
}
.dash {
color: var(--line-2);
}
/* empty + footer */
.empty {
text-align: center;
padding: 40px 20px;
font-size: 14px;
color: var(--muted);
}
.link {
background: none;
border: none;
font: inherit;
color: var(--gold-d);
font-weight: 600;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.inv__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-top: 18px;
padding: 0 4px;
font-size: 12.5px;
color: var(--muted);
}
.inv__foot-note {
color: var(--rose);
}
/* ============ TOAST ============ */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 16px);
background: var(--ink);
color: var(--cream);
font-size: 13.5px;
font-weight: 500;
padding: 12px 20px;
border-radius: 999px;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
max-width: calc(100vw - 32px);
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast__accent {
color: var(--gold-soft);
font-weight: 700;
}
/* ============ RESPONSIVE ============ */
@media (max-width: 920px) {
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.inv__head {
align-items: flex-start;
}
.cards {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.card {
padding: 14px 15px 13px;
border-radius: var(--r-md);
}
.card__value {
font-size: 28px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.tabs {
justify-content: space-between;
}
.search {
max-width: none;
}
.tab {
flex: 1;
justify-content: center;
padding: 8px 10px;
}
.inv__foot {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
}(function () {
"use strict";
/* ============ DATA ============ */
// Realistic but clearly fictional boutique salon retail stock.
var PRODUCTS = [
{ id: "p1", name: "Lumière Repair Shampoo 250ml", brand: "Sève Atelier", sku: "SEV-SH-250", onHand: 14, par: 8, price: 38, supplier: "Sève Distribution", color: "#9d7a4f" },
{ id: "p2", name: "Velvet Hydration Masque 200ml", brand: "Sève Atelier", sku: "SEV-MQ-200", onHand: 5, par: 6, price: 54, supplier: "Sève Distribution", color: "#c9a78f" },
{ id: "p3", name: "Color Gloss 6N — Hazel", brand: "Maison Toné", sku: "MTN-6N-COL", onHand: 0, par: 10, price: 12, supplier: "Toné Pro Supply", color: "#7c5a3a" },
{ id: "p4", name: "Color Gloss 8A — Champagne", brand: "Maison Toné", sku: "MTN-8A-COL", onHand: 22, par: 10, price: 12, supplier: "Toné Pro Supply", color: "#cbb27e" },
{ id: "p5", name: "Developer 20 Vol 1L", brand: "Maison Toné", sku: "MTN-DEV-20", onHand: 9, par: 4, price: 26, supplier: "Toné Pro Supply", color: "#b5bcc2" },
{ id: "p6", name: "Argan Finishing Oil 100ml", brand: "Fleur d'Or", sku: "FDO-OIL-100",onHand: 3, par: 6, price: 48, supplier: "Fleur d'Or Beauté", color: "#b08d57" },
{ id: "p7", name: "Sea-Salt Texture Spray 200ml", brand: "Fleur d'Or", sku: "FDO-TXT-200",onHand: 11, par: 6, price: 32, supplier: "Fleur d'Or Beauté", color: "#a7b3a0" },
{ id: "p8", name: "Thermal Shield Mist 150ml", brand: "Fleur d'Or", sku: "FDO-THM-150",onHand: 6, par: 6, price: 34, supplier: "Fleur d'Or Beauté", color: "#c08a3e" },
{ id: "p9", name: "Aria Ionic Blow Dryer", brand: "Vance Tools", sku: "VNC-DRY-001",onHand: 4, par: 2, price: 189, supplier: "Vance Pro Tools", color: "#3d362f" },
{ id: "p10", name: "Tourmaline Flat Iron 1in", brand: "Vance Tools", sku: "VNC-IRN-100",onHand: 0, par: 3, price: 142, supplier: "Vance Pro Tools", color: "#5a514a" },
{ id: "p11", name: "Boar-Bristle Round Brush 45mm", brand: "Vance Tools", sku: "VNC-BRS-045",onHand: 7, par: 5, price: 44, supplier: "Vance Pro Tools", color: "#8c6d3f" },
{ id: "p12", name: "Rose Scalp Serum 60ml", brand: "Sève Atelier", sku: "SEV-SCP-060", onHand: 2, par: 5, price: 62, supplier: "Sève Distribution", color: "#c9a78f" }
];
var state = { filter: "all", query: "" };
/* ============ HELPERS ============ */
function statusOf(p) {
if (p.onHand <= 0) return "out";
if (p.onHand <= p.par) return "low";
return "ok";
}
function money(n) {
return "$" + n.toLocaleString("en-US");
}
function initials(name) {
var parts = name.replace(/[0-9].*$/, "").trim().split(/\s+/);
return ((parts[0] || "")[0] || "") + ((parts[1] || "")[0] || "");
}
var STATUS_LABEL = { ok: "In stock", low: "Low", out: "Out" };
/* ============ DOM REFS ============ */
var tbody = document.getElementById("tbody");
var emptyState = document.getElementById("emptyState");
var searchInput = document.getElementById("searchInput");
var rowSummary = document.getElementById("rowSummary");
var toastEl = document.getElementById("toast");
var toastTimer = null;
/* ============ TOAST ============ */
function toast(msg, accent) {
toastEl.innerHTML = accent
? msg + ' <span class="toast__accent">' + accent + "</span>"
: msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
/* ============ RENDER ============ */
function matchesFilter(p) {
if (state.filter === "all") return true;
return statusOf(p) === state.filter;
}
function matchesQuery(p) {
if (!state.query) return true;
var q = state.query.toLowerCase();
return (
p.name.toLowerCase().indexOf(q) !== -1 ||
p.brand.toLowerCase().indexOf(q) !== -1 ||
p.sku.toLowerCase().indexOf(q) !== -1 ||
p.supplier.toLowerCase().indexOf(q) !== -1
);
}
function rowHTML(p) {
var st = statusOf(p);
var rowClass = st === "low" ? "row--low" : st === "out" ? "row--out" : "";
var needsReorder = st === "low" || st === "out";
var action = needsReorder
? '<button class="reorder" data-action="reorder" data-id="' + p.id +
'" type="button">Reorder</button>'
: '<span class="dash" aria-hidden="true">—</span>';
return (
'<tr class="' + rowClass + '" data-id="' + p.id + '">' +
'<td class="tbl__product">' +
'<div class="prod">' +
'<span class="prod__sw" style="background:' + p.color + '" aria-hidden="true">' +
initials(p.name) +
"</span>" +
"<span>" +
'<span class="prod__name">' + p.name + "</span>" +
'<span class="prod__brand">' + p.brand + "</span>" +
"</span>" +
"</div>" +
"</td>" +
'<td><span class="sku">' + p.sku + "</span></td>" +
'<td class="tbl__num">' +
'<span class="stepper">' +
'<button class="step" data-action="dec" data-id="' + p.id +
'" type="button" aria-label="Decrease on-hand for ' + p.name + '"' +
(p.onHand <= 0 ? " disabled" : "") + ">−</button>" +
'<span class="stepper__val" data-val="' + p.id + '" aria-live="polite">' +
p.onHand +
"</span>" +
'<button class="step" data-action="inc" data-id="' + p.id +
'" type="button" aria-label="Increase on-hand for ' + p.name + '">+</button>' +
"</span>" +
"</td>" +
'<td class="tbl__num num">' + p.par + "</td>" +
"<td>" +
'<span class="pill pill--' + st + '" data-pill="' + p.id + '">' +
STATUS_LABEL[st] +
"</span>" +
"</td>" +
'<td class="tbl__num retail">' + money(p.price) + "</td>" +
'<td><span class="supplier">' + p.supplier + "</span></td>" +
'<td class="tbl__act act-cell">' + action + "</td>" +
"</tr>"
);
}
function render() {
var visible = PRODUCTS.filter(function (p) {
return matchesFilter(p) && matchesQuery(p);
});
tbody.innerHTML = visible.map(rowHTML).join("");
emptyState.hidden = visible.length !== 0;
rowSummary.textContent =
"Showing " + visible.length + " of " + PRODUCTS.length + " products";
}
function updateStats() {
var low = 0, out = 0, retail = 0;
PRODUCTS.forEach(function (p) {
var st = statusOf(p);
if (st === "low") low++;
if (st === "out") out++;
retail += p.onHand * p.price;
});
document.getElementById("statSkus").textContent = PRODUCTS.length;
document.getElementById("statLow").textContent = low;
document.getElementById("statOut").textContent = out;
document.getElementById("statValue").textContent = money(retail);
document.querySelector('[data-count="all"]').textContent = PRODUCTS.length;
document.querySelector('[data-count="low"]').textContent = low;
document.querySelector('[data-count="out"]').textContent = out;
}
function refresh() {
updateStats();
render();
}
/* ============ INTERACTIONS ============ */
function adjust(id, delta) {
var p = PRODUCTS.find(function (x) { return x.id === id; });
if (!p) return;
var next = p.onHand + delta;
if (next < 0) return;
p.onHand = next;
// If the row is still visible under the current filter, update it in place
// with a flash; otherwise the filter no longer matches, so re-render.
var row = tbody.querySelector('tr[data-id="' + id + '"]');
var stillVisible = matchesFilter(p) && matchesQuery(p);
if (row && stillVisible) {
var st = statusOf(p);
row.className = st === "low" ? "row--low" : st === "out" ? "row--out" : "";
row.querySelector('[data-val="' + id + '"]').textContent = p.onHand;
var pill = row.querySelector('[data-pill="' + id + '"]');
pill.className = "pill pill--" + st;
pill.textContent = STATUS_LABEL[st];
var dec = row.querySelector('[data-action="dec"]');
dec.disabled = p.onHand <= 0;
// refresh the action cell (reorder appears/disappears)
var actCell = row.querySelector(".act-cell");
if (st === "low" || st === "out") {
if (!actCell.querySelector(".reorder")) {
actCell.innerHTML =
'<button class="reorder" data-action="reorder" data-id="' + id +
'" type="button">Reorder</button>';
}
} else {
actCell.innerHTML = '<span class="dash" aria-hidden="true">—</span>';
}
row.classList.remove("flash");
void row.offsetWidth; // restart animation
row.classList.add("flash");
updateStats();
} else {
refresh();
}
}
function reorder(id, btn) {
var p = PRODUCTS.find(function (x) { return x.id === id; });
if (!p || btn.classList.contains("is-done")) return;
var qty = Math.max(p.par * 2 - p.onHand, p.par);
btn.classList.add("is-done");
btn.textContent = "Ordered";
toast("Reorder placed · " + qty + " × " + p.name + " from", p.supplier);
}
/* delegated clicks on the table */
tbody.addEventListener("click", function (e) {
var t = e.target.closest("[data-action]");
if (!t) return;
var action = t.getAttribute("data-action");
var id = t.getAttribute("data-id");
if (action === "inc") adjust(id, 1);
else if (action === "dec") adjust(id, -1);
else if (action === "reorder") reorder(id, t);
});
/* filter tabs */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
state.filter = tab.getAttribute("data-filter");
render();
});
});
/* search */
searchInput.addEventListener("input", function () {
state.query = searchInput.value.trim();
render();
});
/* clear filters from empty state */
document.getElementById("clearBtn").addEventListener("click", function () {
state.filter = "all";
state.query = "";
searchInput.value = "";
tabs.forEach(function (t) {
var on = t.getAttribute("data-filter") === "all";
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
});
render();
});
/* export (toast only — no backend) */
document.getElementById("exportBtn").addEventListener("click", function () {
toast("Exported " + PRODUCTS.length + " SKUs to", "inventory.csv");
});
/* ============ INIT ============ */
refresh();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Retail Inventory · Maison Lumière Salon</title>
</head>
<body>
<main class="inv" aria-label="Retail inventory">
<!-- ================= HEADER ================= -->
<header class="inv__head">
<div class="inv__brand">
<p class="kicker">Maison Lumière Salon</p>
<h1 class="inv__title">Retail Inventory</h1>
<p class="inv__sub">Back-bar & shelf stock · updated live</p>
</div>
<div class="inv__meta">
<span class="chip">
<span class="chip__dot" aria-hidden="true"></span>
Boutique · Sève Atelier
</span>
<button class="btn btn--ghost" type="button" id="exportBtn">
Export CSV
</button>
</div>
</header>
<!-- ================= SUMMARY CARDS ================= -->
<section class="cards" aria-label="Inventory summary">
<article class="card">
<p class="card__label">Active SKUs</p>
<p class="card__value" id="statSkus">0</p>
<p class="card__note">distinct products tracked</p>
</article>
<article class="card card--low">
<p class="card__label">Low stock</p>
<p class="card__value" id="statLow">0</p>
<p class="card__note">at or below par level</p>
</article>
<article class="card card--out">
<p class="card__label">Out of stock</p>
<p class="card__value" id="statOut">0</p>
<p class="card__note">needs reorder now</p>
</article>
<article class="card card--value">
<p class="card__label">Retail value</p>
<p class="card__value" id="statValue">$0</p>
<p class="card__note">on-hand at shelf price</p>
</article>
</section>
<!-- ================= TOOLBAR ================= -->
<section class="toolbar" aria-label="Filter and search">
<div class="tabs" role="tablist" aria-label="Stock status filter">
<button class="tab is-active" role="tab" aria-selected="true" data-filter="all" type="button">
All <span class="tab__count" data-count="all">0</span>
</button>
<button class="tab" role="tab" aria-selected="false" data-filter="low" type="button">
Low <span class="tab__count" data-count="low">0</span>
</button>
<button class="tab" role="tab" aria-selected="false" data-filter="out" type="button">
Out <span class="tab__count" data-count="out">0</span>
</button>
</div>
<div class="search">
<svg class="search__icon" viewBox="0 0 20 20" aria-hidden="true" width="16" height="16">
<path d="M9 3a6 6 0 1 0 3.7 10.7l3.3 3.3 1.4-1.4-3.3-3.3A6 6 0 0 0 9 3Zm0 2a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z" fill="currentColor"/>
</svg>
<input
type="search"
id="searchInput"
class="search__input"
placeholder="Search product, brand or SKU…"
aria-label="Search inventory"
autocomplete="off"
/>
</div>
</section>
<!-- ================= TABLE ================= -->
<section class="panel" aria-label="Product list">
<div class="table-wrap" role="region" aria-label="Inventory table" tabindex="0">
<table class="tbl">
<thead>
<tr>
<th scope="col" class="tbl__product">Product</th>
<th scope="col">SKU</th>
<th scope="col" class="tbl__num">On hand</th>
<th scope="col" class="tbl__num">Par</th>
<th scope="col">Status</th>
<th scope="col" class="tbl__num">Retail</th>
<th scope="col">Supplier</th>
<th scope="col" class="tbl__act">Action</th>
</tr>
</thead>
<tbody id="tbody"><!-- rows injected by script.js --></tbody>
</table>
</div>
<p class="empty" id="emptyState" hidden>
No products match your filter. <button type="button" class="link" id="clearBtn">Clear filters</button>
</p>
</section>
<footer class="inv__foot">
<span id="rowSummary">Showing 0 of 0 products</span>
<span class="inv__foot-note">Steppers adjust on-hand and recompute status instantly.</span>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Retail Inventory
The stockroom view for Maison Lumière, where the front-of-house polish meets back-office precision. A serif title sits above four editorial summary cards — active SKUs, low stock, out of stock and total retail value — each lit by a thin gold or accent hairline so the numbers that matter read at a glance. Below, a refined table catalogues a dozen real-feeling products: repair shampoos, color gloss tubes, developer, finishing oils and pro tools, each with a tinted swatch, brand, monospaced SKU, par level and supplier.
Every row is alive. The inline plus-minus steppers adjust on-hand counts and instantly recompute the status pill — In stock, Low or Out — re-tinting the row and toggling a gold Reorder button as levels cross their par. Filter tabs for All, Low and Out carry live counts, a search box scans product, brand, SKU and supplier, and the summary cards and footer tally update in the same breath. Reordering or exporting raises a quiet confirmation toast.
Built in dependency-free vanilla JavaScript: delegated event handling, in-place row updates with a soft flash, accessible steppers and tabs with aria labels and visible focus, and a reusable toast() helper. The palette is rose-gold on cream in Cormorant Garamond and Inter, with tabular figures throughout and a layout that holds its composure down to 360px.