Salon — Client Notes
A luxe client-notes panel for a boutique salon stylist. A profile header pairs an initialed avatar with name, VIP status and live visit count, while a red-flagged callout keeps allergies and sensitivities impossible to miss. Removable preference chips capture quick cues, a product-history list tracks formulas and home care, and a timestamped notes timeline prepends each new entry newest-first. Everything updates in memory with live counts and a gold toast on every save.
MCP
代码
: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;
--shadow-sm: 0 1px 2px rgba(28, 24, 20, 0.06),
0 4px 14px rgba(28, 24, 20, 0.05);
--shadow-md: 0 6px 24px rgba(28, 24, 20, 0.09),
0 24px 60px rgba(28, 24, 20, 0.08);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--sans);
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 520px at 88% -10%, var(--rose-soft), transparent 60%),
radial-gradient(760px 460px at -10% 110%, var(--gold-soft), transparent 55%),
var(--bg);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.shell {
max-width: 900px;
margin: 0 auto;
padding: clamp(20px, 5vw, 56px) clamp(16px, 4vw, 32px);
}
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
padding: clamp(20px, 4vw, 38px);
}
/* ---------- Profile header ---------- */
.profile {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 22px;
padding-bottom: 26px;
border-bottom: 1px solid var(--line);
}
.profile__id {
display: flex;
align-items: center;
gap: 18px;
}
.avatar {
flex: none;
display: grid;
place-items: center;
width: 66px;
height: 66px;
border-radius: 50%;
font-family: var(--serif);
font-weight: 600;
font-size: 24px;
letter-spacing: 0.03em;
color: var(--white);
background: linear-gradient(150deg, var(--rose), var(--gold-d));
box-shadow: var(--shadow-sm);
border: 2px solid var(--white);
outline: 1px solid var(--gold-soft);
}
.eyebrow {
margin: 0 0 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--gold-d);
}
#client-name {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: clamp(28px, 5vw, 38px);
line-height: 1.05;
color: var(--ink);
}
.profile__sub {
margin: 6px 0 0;
font-size: 13px;
color: var(--muted);
}
.profile__sub strong {
color: var(--ink-2);
font-weight: 600;
}
.dot {
display: inline-block;
width: 7px;
height: 7px;
margin-right: 6px;
border-radius: 50%;
background: var(--ok);
vertical-align: middle;
}
.profile__stats {
display: flex;
gap: 10px;
margin: 0;
}
.stat {
text-align: center;
padding: 10px 16px;
border-radius: var(--r-md);
background: var(--cream);
border: 1px solid var(--line);
min-width: 72px;
}
.stat dt {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.stat dd {
margin: 3px 0 0;
font-family: var(--serif);
font-weight: 600;
font-size: 22px;
color: var(--ink);
}
/* ---------- Allergy callout ---------- */
.callout {
display: flex;
gap: 14px;
margin: 26px 0 0;
padding: 16px 18px;
border-radius: var(--r-md);
background: linear-gradient(180deg, #fdf3ef, #fbeae4);
border: 1px solid rgba(179, 80, 62, 0.28);
border-left: 3px solid var(--danger);
}
.callout__icon {
flex: none;
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--danger);
color: var(--white);
font-weight: 700;
font-size: 15px;
}
.callout__title {
margin: 1px 0 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--danger);
}
.callout__list {
margin: 0;
padding-left: 18px;
color: var(--ink-2);
font-size: 13.5px;
}
.callout__list li + li {
margin-top: 4px;
}
/* ---------- Grid of cards ---------- */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 22px;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px;
box-shadow: var(--shadow-sm);
}
.card--notes {
margin-top: 18px;
}
.card__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.card__head h2 {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 22px;
color: var(--ink);
}
.count {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold-d);
background: var(--gold-soft);
padding: 3px 9px;
border-radius: 999px;
}
.card__hint {
margin: 8px 0 14px;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Chips ---------- */
.chips {
list-style: none;
margin: 0 0 14px;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 30px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 8px 6px 13px;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
background: var(--rose-soft);
border: 1px solid rgba(201, 167, 143, 0.5);
border-radius: 999px;
animation: pop 0.2s ease;
}
.chip__x {
display: grid;
place-items: center;
width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 50%;
background: rgba(28, 24, 20, 0.08);
color: var(--ink-2);
font-size: 13px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.chip__x:hover,
.chip__x:focus-visible {
background: var(--danger);
color: var(--white);
outline: none;
}
.chip-empty {
font-size: 13px;
color: var(--muted);
font-style: italic;
}
.chip-form {
display: flex;
gap: 8px;
}
.chip-form input {
flex: 1;
min-width: 0;
}
/* ---------- Inputs ---------- */
input[type="text"],
textarea {
width: 100%;
font-family: var(--sans);
font-size: 14px;
color: var(--ink);
background: var(--cream);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
transition: border-color 0.15s ease, box-shadow 0.15s ease,
background 0.15s ease;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
background: var(--white);
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.18);
}
textarea {
resize: vertical;
min-height: 64px;
}
::placeholder {
color: var(--muted);
}
/* ---------- Buttons ---------- */
.btn {
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
padding: 10px 16px;
border-radius: var(--r-sm);
border: 1px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: transform 0.12s ease, box-shadow 0.15s ease,
background 0.15s ease, color 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--gold {
color: var(--white);
background: linear-gradient(160deg, var(--gold), var(--gold-d));
box-shadow: var(--shadow-sm);
}
.btn--gold:hover {
box-shadow: 0 6px 18px rgba(140, 109, 63, 0.32);
}
.btn--ghost {
color: var(--gold-d);
background: var(--white);
border-color: var(--line-2);
}
.btn--ghost:hover {
border-color: var(--gold);
background: var(--cream);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.3);
}
/* ---------- Product history ---------- */
.products {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.product {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 11px 4px;
border-top: 1px solid var(--line);
}
.product:first-child {
border-top: none;
}
.product__swatch {
flex: none;
width: 12px;
height: 12px;
margin-top: 4px;
border-radius: 3px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.product__name {
font-weight: 600;
font-size: 14px;
color: var(--ink);
}
.product__detail {
font-size: 12.5px;
color: var(--muted);
}
.product__tag {
margin-left: auto;
flex: none;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gold-d);
background: var(--gold-soft);
padding: 3px 8px;
border-radius: 999px;
align-self: center;
}
/* ---------- Notes timeline ---------- */
.note-form__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 10px;
}
.note-form__hint {
font-size: 12px;
color: var(--muted);
}
.timeline {
list-style: none;
margin: 20px 0 0;
padding: 0;
position: relative;
}
.timeline::before {
content: "";
position: absolute;
left: 5px;
top: 6px;
bottom: 6px;
width: 1px;
background: var(--line-2);
}
.entry {
position: relative;
padding: 0 0 18px 26px;
animation: rise 0.28s ease;
}
.entry:last-child {
padding-bottom: 0;
}
.entry::before {
content: "";
position: absolute;
left: 1px;
top: 4px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--gold);
box-shadow: 0 0 0 3px var(--gold-soft);
}
.entry__time {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--gold-d);
}
.entry__text {
margin: 4px 0 0;
font-size: 14px;
color: var(--ink-2);
white-space: pre-wrap;
word-break: break-word;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
z-index: 50;
display: flex;
align-items: center;
gap: 9px;
padding: 12px 20px;
font-size: 13.5px;
font-weight: 500;
color: var(--white);
background: var(--ink);
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast::before {
content: "✓";
color: var(--gold);
font-weight: 700;
}
.toast[hidden] {
display: flex;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Animations ---------- */
@keyframes pop {
from {
transform: scale(0.92);
opacity: 0;
}
}
@keyframes rise {
from {
transform: translateY(6px);
opacity: 0;
}
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
font-size: 14.5px;
}
.panel {
border-radius: var(--r-md);
}
.profile {
gap: 16px;
}
.profile__id {
gap: 14px;
}
.avatar {
width: 54px;
height: 54px;
font-size: 20px;
}
.profile__stats {
width: 100%;
justify-content: space-between;
}
.stat {
flex: 1;
min-width: 0;
padding: 9px 8px;
}
.grid {
grid-template-columns: 1fr;
}
.note-form__row {
flex-direction: column;
align-items: stretch;
}
.note-form__row .btn {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- In-memory state ---------- */
const state = {
preferences: [
"Prefers cooler tones",
"No fragrance",
"Gloss finish, not matte",
"Books mornings only",
],
products: [
{
name: "Olaplex No.3",
detail: "Bond builder · pre-color treatment",
tag: "In use",
swatch: "#c9a78f",
},
{
name: "Wella Koleston 7/1",
detail: "Ash base · 20 vol developer",
tag: "Formula",
swatch: "#8a7d70",
},
{
name: "Kérastase Blond Absolu",
detail: "Purple mask · every 3rd wash",
tag: "Home",
swatch: "#b6a0c4",
},
{
name: "Davines OI Oil",
detail: "Finishing — fragrance-free swap",
tag: "Swapped",
swatch: "#b08d57",
},
],
notes: [
{
ts: "May 18, 2026 · 10:42 AM",
text: "Half-head babylights + gloss refresh. Loved the cooler result — keep developer at 20 vol max next time.",
},
{
ts: "Mar 02, 2026 · 9:15 AM",
text: "Patch test passed for new ash line. Scalp calm throughout. Booked 8-week return.",
},
],
};
/* ---------- Helpers ---------- */
const $ = (sel) => document.querySelector(sel);
const el = (tag, cls) => {
const n = document.createElement(tag);
if (cls) n.className = cls;
return n;
};
let toastTimer = null;
const toastNode = $("#toast");
function toast(msg) {
toastNode.textContent = msg;
toastNode.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastNode.classList.remove("show"), 2400);
}
function stamp() {
const d = new Date();
const date = d.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
});
const time = d.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
return date + " · " + time;
}
/* ---------- Preferences ---------- */
const chipList = $("#chip-list");
const prefCount = $("#pref-count");
function renderChips() {
chipList.innerHTML = "";
if (state.preferences.length === 0) {
const empty = el("li", "chip-empty");
empty.textContent = "No preferences saved yet.";
chipList.appendChild(empty);
} else {
state.preferences.forEach((pref, i) => {
const li = el("li", "chip");
const label = document.createElement("span");
label.textContent = pref;
const x = el("button", "chip__x");
x.type = "button";
x.innerHTML = "×";
x.setAttribute("aria-label", "Remove preference: " + pref);
x.dataset.index = String(i);
li.appendChild(label);
li.appendChild(x);
chipList.appendChild(li);
});
}
const n = state.preferences.length;
prefCount.textContent = n + (n === 1 ? " cue" : " cues");
}
chipList.addEventListener("click", (e) => {
const btn = e.target.closest(".chip__x");
if (!btn) return;
const i = Number(btn.dataset.index);
const [removed] = state.preferences.splice(i, 1);
renderChips();
toast("Removed “" + removed + "”");
});
$("#chip-form").addEventListener("submit", (e) => {
e.preventDefault();
const input = $("#chip-input");
const value = input.value.trim();
if (!value) return;
const exists = state.preferences.some(
(p) => p.toLowerCase() === value.toLowerCase()
);
if (exists) {
toast("That preference is already saved");
input.select();
return;
}
state.preferences.push(value);
input.value = "";
input.focus();
renderChips();
toast("Preference added");
});
/* ---------- Product history ---------- */
function renderProducts() {
const list = $("#product-list");
list.innerHTML = "";
state.products.forEach((p) => {
const li = el("li", "product");
const swatch = el("span", "product__swatch");
swatch.style.background = p.swatch;
swatch.setAttribute("aria-hidden", "true");
const body = el("div");
const name = el("div", "product__name");
name.textContent = p.name;
const detail = el("div", "product__detail");
detail.textContent = p.detail;
body.appendChild(name);
body.appendChild(detail);
const tag = el("span", "product__tag");
tag.textContent = p.tag;
li.appendChild(swatch);
li.appendChild(body);
li.appendChild(tag);
list.appendChild(li);
});
}
/* ---------- Notes ---------- */
const noteList = $("#note-list");
const noteCount = $("#note-count");
function renderNotes() {
noteList.innerHTML = "";
state.notes.forEach((note) => {
const li = el("li", "entry");
const time = el("p", "entry__time");
time.textContent = note.ts;
const text = el("p", "entry__text");
text.textContent = note.text;
li.appendChild(time);
li.appendChild(text);
noteList.appendChild(li);
});
const n = state.notes.length;
noteCount.textContent = n + (n === 1 ? " entry" : " entries");
}
$("#note-form").addEventListener("submit", (e) => {
e.preventDefault();
const input = $("#note-input");
const value = input.value.trim();
if (!value) {
toast("Write a note first");
input.focus();
return;
}
state.notes.unshift({ ts: stamp(), text: value });
input.value = "";
input.focus();
renderNotes();
toast("Note saved");
});
/* ---------- Init ---------- */
renderChips();
renderProducts();
renderNotes();
$("#visit-count").textContent = String(24);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Client Notes · Maison Lumière Salon</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=Cormorant+Garamond:wght@500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell" aria-labelledby="client-name">
<article class="panel">
<!-- Profile header -->
<header class="profile">
<div class="profile__id">
<span class="avatar" aria-hidden="true">AV</span>
<div class="profile__meta">
<p class="eyebrow">Client file</p>
<h1 id="client-name">Aria Vance</h1>
<p class="profile__sub">
<span class="dot" aria-hidden="true"></span>
VIP member · Stylist <strong>Noémie Roux</strong>
</p>
</div>
</div>
<dl class="profile__stats">
<div class="stat">
<dt>Visits</dt>
<dd id="visit-count">24</dd>
</div>
<div class="stat">
<dt>Since</dt>
<dd>2021</dd>
</div>
<div class="stat">
<dt>Last seen</dt>
<dd>May 18</dd>
</div>
</dl>
</header>
<!-- Allergy / sensitivity callout -->
<section
class="callout"
role="alert"
aria-labelledby="allergy-title"
>
<span class="callout__icon" aria-hidden="true">!</span>
<div class="callout__body">
<p id="allergy-title" class="callout__title">
Allergies & sensitivities
</p>
<ul class="callout__list" id="allergy-list">
<li>PPD (para-phenylenediamine) — patch test required</li>
<li>Strong fragrance triggers migraines</li>
<li>Sensitive scalp — avoid high-volume developer</li>
</ul>
</div>
</section>
<div class="grid">
<!-- Preferences -->
<section class="card" aria-labelledby="pref-title">
<div class="card__head">
<h2 id="pref-title">Preferences</h2>
<span class="count" id="pref-count" aria-live="polite"></span>
</div>
<p class="card__hint">
Quick cues for every visit. Remove a chip with the × or its
Delete key.
</p>
<ul class="chips" id="chip-list" aria-label="Client preferences"></ul>
<form class="chip-form" id="chip-form" autocomplete="off">
<label class="sr-only" for="chip-input">Add a preference</label>
<input
id="chip-input"
name="chip"
type="text"
placeholder="Add a preference…"
maxlength="40"
/>
<button type="submit" class="btn btn--ghost">Add</button>
</form>
</section>
<!-- Product history -->
<section class="card" aria-labelledby="prod-title">
<div class="card__head">
<h2 id="prod-title">Product history</h2>
</div>
<p class="card__hint">Formulas & lines that worked.</p>
<ul class="products" id="product-list"></ul>
</section>
</div>
<!-- Notes -->
<section class="card card--notes" aria-labelledby="notes-title">
<div class="card__head">
<h2 id="notes-title">Notes</h2>
<span class="count" id="note-count" aria-live="polite"></span>
</div>
<form class="note-form" id="note-form" autocomplete="off">
<label class="sr-only" for="note-input">New note</label>
<textarea
id="note-input"
name="note"
rows="3"
placeholder="Add a timestamped note for the next visit…"
></textarea>
<div class="note-form__row">
<span class="note-form__hint">Saved entries appear newest-first.</span>
<button type="submit" class="btn btn--gold">Save note</button>
</div>
</form>
<ol class="timeline" id="note-list" aria-label="Saved notes"></ol>
</section>
</article>
</main>
<div
id="toast"
class="toast"
role="status"
aria-live="polite"
hidden
></div>
<script src="script.js"></script>
</body>
</html>Client Notes
The working file a Maison Lumière stylist opens before every appointment. A serif client name sits beside an initialed rose-gold avatar, a VIP pill and a live visit count, so the relationship is legible at a glance. Directly beneath, a red-bordered callout holds allergies and sensitivities — patch-test flags, fragrance triggers, scalp notes — given the prominence safety deserves.
Preferences live as soft rose chips you can grow or trim: type a cue and it pops into place, tap the × to remove one, and a live counter and toast confirm every change while gently blocking duplicates. A product-history list pairs colored swatches with formulas and home-care lines, and the notes timeline prepends each saved entry with a fresh timestamp, newest-first, along a thin gold thread.
Built in vanilla JavaScript with no dependencies: delegated chip handling, accessible labels and aria-live counts, a reusable toast() helper, and a rose-gold-on-cream palette set in Cormorant Garamond and Inter. Fully responsive down to 360px.