Componentes UI Medium
Hotel PMS — Reservation Edit
Modal/dialog widget for editing an existing reservation: dates · room category · rate plan · guests · special requests, with live price recalculation.
Abrir no Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--navy: #1a2b4a;
--navy-d: #0f1d36;
--navy-2: #2d4570;
--cream: #f7f3eb;
--cream-2: #ece5d4;
--bone: #fbf8f2;
--gold: #c9a649;
--gold-light: #e0c378;
--gold-d: #a88a2e;
--ink: #161e2c;
--ink-2: #2e3a52;
--warm-gray: #6c7280;
--line: rgba(22, 30, 44, 0.1);
--line-strong: rgba(22, 30, 44, 0.2);
--success: #4a7752;
--danger: #b34232;
--warning: #d99020;
--font-display: "Cormorant Garamond", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
font-family: var(--font-body);
color: var(--ink);
-webkit-font-smoothing: antialiased;
background: radial-gradient(circle at 18% 14%, rgba(201, 166, 73, 0.18), transparent 55%),
radial-gradient(circle at 84% 78%, rgba(74, 109, 160, 0.16), transparent 60%),
linear-gradient(180deg, #14213b 0%, #0f1d36 100%);
}
.bg {
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px;
}
/* ── Dialog ── */
.dialog {
width: min(960px, 100%);
background: var(--bone);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 40px 100px rgba(15, 29, 54, 0.55);
display: flex;
flex-direction: column;
max-height: 92vh;
}
.d-head {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 22px 28px 18px;
border-bottom: 1px solid var(--line);
}
.kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--gold-d);
font-weight: 700;
font-family: var(--font-mono);
}
.d-head h2 {
font-family: var(--font-display);
font-size: 1.65rem;
font-weight: 700;
letter-spacing: -0.005em;
margin-top: 2px;
color: var(--navy-d);
}
.d-head h2 span {
color: var(--gold-d);
}
.d-close {
background: transparent;
border: 1px solid var(--line-strong);
width: 36px;
height: 36px;
border-radius: 999px;
font-size: 1.4rem;
cursor: pointer;
color: var(--ink-2);
line-height: 1;
}
.d-close:hover {
border-color: var(--danger);
color: var(--danger);
}
.d-body {
display: grid;
grid-template-columns: 1fr 300px;
gap: 24px;
padding: 24px 28px;
overflow-y: auto;
}
/* ── Form ── */
.form fieldset {
border: none;
margin-bottom: 18px;
padding: 14px 16px;
background: var(--cream);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.form legend {
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-d);
font-weight: 700;
padding: 0 6px;
background: var(--cream);
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field span {
font-size: 0.74rem;
color: var(--warm-gray);
font-weight: 600;
}
.field input,
.field select,
.form textarea {
font-family: inherit;
font-size: 0.9rem;
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
padding: 9px 12px;
color: var(--ink);
outline: none;
}
.field input:focus,
.field select:focus,
.form textarea:focus {
border-color: var(--gold);
}
.nights {
margin-top: 10px;
font-size: 0.86rem;
color: var(--ink-2);
}
.nights strong {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
color: var(--navy-d);
margin-right: 4px;
}
.form textarea {
width: 100%;
resize: vertical;
font-family: inherit;
margin-top: 10px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
background: var(--bone);
border: 1px solid var(--line-strong);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
padding: 7px 12px;
border-radius: 999px;
cursor: pointer;
}
.chip:hover {
border-color: var(--gold);
color: var(--gold-d);
}
.chip.is-on {
background: var(--gold);
color: var(--navy-d);
border-color: var(--gold);
}
/* ── Summary ── */
.summary {
background: var(--navy);
color: var(--bone);
border-radius: var(--r-md);
padding: 22px;
align-self: start;
position: sticky;
top: 0;
}
.summary h3 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.005em;
margin-bottom: 14px;
color: var(--gold-light);
}
.lines {
list-style: none;
}
.lines li {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 7px 0;
font-size: 0.82rem;
color: rgba(251, 248, 242, 0.78);
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}
.lines li:last-child {
border-bottom: none;
}
.lines li span:last-child {
font-family: var(--font-mono);
font-weight: 600;
color: var(--bone);
}
.lines li.total {
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.18);
border-bottom: none;
font-weight: 700;
}
.lines li.total span:first-child {
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--gold-light);
}
.lines li.total span:last-child {
font-family: var(--font-display);
font-size: 1.7rem;
color: var(--gold-light);
}
.deposit {
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed rgba(255, 255, 255, 0.18);
font-size: 0.78rem;
color: rgba(251, 248, 242, 0.65);
}
.deposit strong {
color: var(--bone);
font-weight: 600;
}
.diff {
margin-top: 14px;
background: rgba(201, 166, 73, 0.15);
border: 1px solid rgba(201, 166, 73, 0.4);
border-radius: var(--r-sm);
padding: 10px 12px;
font-size: 0.8rem;
}
.diff strong {
display: block;
font-weight: 600;
font-size: 0.86rem;
color: var(--gold-light);
}
.diff strong span {
font-family: var(--font-mono);
}
.diff small {
font-size: 0.74rem;
color: rgba(251, 248, 242, 0.6);
display: block;
margin-top: 4px;
}
/* ── Footer ── */
.d-foot {
border-top: 1px solid var(--line);
background: var(--cream);
padding: 14px 24px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.d-foot-right {
display: flex;
gap: 8px;
}
.ghost,
.primary {
font-family: inherit;
font-weight: 600;
font-size: 0.86rem;
padding: 10px 18px;
border-radius: 999px;
cursor: pointer;
}
.ghost {
background: transparent;
border: 1px solid var(--line-strong);
color: var(--ink-2);
}
.ghost:hover {
border-color: var(--navy-2);
color: var(--navy-d);
}
.primary {
background: var(--navy);
color: var(--bone);
border: none;
}
.primary:hover {
background: var(--navy-d);
}
.d-foot .ghost:first-child:hover {
border-color: var(--danger);
color: var(--danger);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 11px 22px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: 0 10px 30px rgba(22, 30, 44, 0.18);
}
@media (max-width: 880px) {
.d-body {
grid-template-columns: 1fr;
}
.summary {
position: static;
}
.d-foot {
flex-direction: column;
align-items: stretch;
}
.d-foot-right {
justify-content: flex-end;
}
}const ORIGINAL_TOTAL = 1205.5;
const CITY_PER_GUEST_NIGHT = 1.65;
const fmt = (n) =>
`€${n.toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const fmtSigned = (n) =>
`${n < 0 ? "−" : "+"}€${Math.abs(n).toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const $ = (id) => document.getElementById(id);
const arrive = $("arrive");
const depart = $("depart");
const cat = $("cat");
const plan = $("plan");
const adults = $("adults");
const children = $("children");
const cots = $("cots");
const toast = $("toast");
function diffNights() {
const a = new Date(arrive.value);
const d = new Date(depart.value);
if (isNaN(a) || isNaN(d)) return 0;
return Math.max(1, Math.round((d - a) / 86400000));
}
function calc() {
const nights = diffNights();
const rate = parseFloat(cat.selectedOptions[0].dataset.rate);
const mult = parseFloat(plan.selectedOptions[0].dataset.mult);
const guests =
Math.max(1, parseInt(adults.value || 1, 10)) + Math.max(0, parseInt(children.value || 0, 10));
const base = nights * rate;
const discount = base * (mult - 1);
const city = guests * CITY_PER_GUEST_NIGHT * nights;
const total = base + discount + city;
return { nights, rate, mult, guests, base, discount, city, total };
}
function render() {
const { nights, rate, mult, guests, base, discount, city, total } = calc();
$("nights").textContent = nights;
$("sNights").textContent = nights;
$("sNights2").textContent = nights;
$("sRate").textContent = rate;
$("sBase").textContent = fmt(base);
$("sMult").textContent = mult === 1 ? "no change" : `${Math.round((mult - 1) * 100)}%`;
$("sDisc").textContent = discount === 0 ? "—" : fmtSigned(discount);
$("sGuestsLbl").textContent = guests;
$("sCity").textContent = fmt(city);
$("sTotal").textContent = fmt(total);
$("diffAmt").textContent = fmtSigned(total - ORIGINAL_TOTAL);
}
[arrive, depart, cat, plan, adults, children, cots].forEach((el) =>
el.addEventListener("input", render)
);
document.getElementById("chips").addEventListener("click", (e) => {
const c = e.target.closest(".chip");
if (!c) return;
c.classList.toggle("is-on");
});
document.getElementById("save").addEventListener("click", () => {
toast.textContent = `Saved · confirmation re-issued (${fmtSigned(calc().total - ORIGINAL_TOTAL)})`;
toast.hidden = false;
clearTimeout(window.__t);
window.__t = setTimeout(() => (toast.hidden = true), 2000);
});
document.querySelector(".d-close").addEventListener("click", () => {
toast.textContent = "Closed without saving";
toast.hidden = false;
clearTimeout(window.__t);
window.__t = setTimeout(() => (toast.hidden = true), 1400);
});
render();<!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@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Edit Reservation · Aurelia Hotels</title>
</head>
<body>
<div class="bg">
<section class="dialog" role="dialog" aria-modal="true" aria-labelledby="dTitle">
<header class="d-head">
<div>
<p class="kicker">AUR-21847</p>
<h2 id="dTitle">Edit reservation · <span>Aiko Tanaka</span></h2>
</div>
<button class="d-close" aria-label="Close">×</button>
</header>
<div class="d-body">
<form class="form" id="form">
<fieldset>
<legend>Stay dates</legend>
<div class="grid-2">
<label class="field"><span>Arrival</span>
<input id="arrive" type="date" value="2026-05-24" />
</label>
<label class="field"><span>Departure</span>
<input id="depart" type="date" value="2026-05-29" />
</label>
</div>
<p class="nights"><strong id="nights">5</strong> nights · <span id="checkTime">15:00 → 12:00</span></p>
</fieldset>
<fieldset>
<legend>Room & rate</legend>
<label class="field"><span>Room category</span>
<select id="cat">
<option value="single" data-rate="142">Single Standard · €142/night</option>
<option value="double" data-rate="184">Classic Double · €184/night</option>
<option value="twin" data-rate="184">Twin Standard · €184/night</option>
<option value="deluxe" data-rate="232">Deluxe Double · €232/night</option>
<option value="junior" data-rate="241" selected>Junior Suite · €241/night</option>
<option value="exec" data-rate="412">Executive Suite · €412/night</option>
<option value="ph" data-rate="612">Penthouse · €612/night</option>
</select>
</label>
<label class="field"><span>Rate plan</span>
<select id="plan">
<option value="bar" data-mult="1">Flex BAR (refundable)</option>
<option value="promo" data-mult="0.88" selected>Promo · −12% non-refundable</option>
<option value="ci" data-mult="0.78">Corporate Innovo · −22%</option>
<option value="bb" data-mult="1.06">B&B · +€18/night</option>
</select>
</label>
</fieldset>
<fieldset>
<legend>Guests</legend>
<div class="grid-3">
<label class="field"><span>Adults</span>
<input id="adults" type="number" min="1" max="6" value="2" />
</label>
<label class="field"><span>Children</span>
<input id="children" type="number" min="0" max="4" value="0" />
</label>
<label class="field"><span>Cots</span>
<input id="cots" type="number" min="0" max="2" value="0" />
</label>
</div>
</fieldset>
<fieldset>
<legend>Special requests</legend>
<div class="chips" id="chips">
<button type="button" class="chip" data-tag="high-floor">High floor</button>
<button type="button" class="chip is-on" data-tag="quiet">Quiet room</button>
<button type="button" class="chip" data-tag="late-checkin">Late check-in</button>
<button type="button" class="chip is-on" data-tag="vegan">Vegan breakfast</button>
<button type="button" class="chip" data-tag="airport">Airport pickup</button>
<button type="button" class="chip" data-tag="celebration">Anniversary setup</button>
</div>
<textarea id="notes" placeholder="Internal notes (visible to staff only)…" rows="3">Guest prefers king bed; arrival possibly after 22:00.</textarea>
</fieldset>
</form>
<aside class="summary">
<h3>Rate summary</h3>
<ul class="lines">
<li><span><span id="sNights">5</span> nights × €<span id="sRate">241</span></span><span id="sBase">€1,205.00</span></li>
<li><span>Promo · <span id="sMult">−12%</span></span><span id="sDisc">−€144.60</span></li>
<li><span>City tax · <span id="sGuestsLbl">2</span> × €1.65 × <span id="sNights2">5</span></span><span id="sCity">€16.50</span></li>
<li class="total"><span>Total stay</span><span id="sTotal">€1,076.90</span></li>
</ul>
<div class="deposit">
<p><strong>Deposit on file:</strong> €100 · captured on Visa •••• 4421</p>
</div>
<div class="diff" id="diff">
<strong>Δ vs original: <span id="diffAmt">−€128.60</span></strong>
<small>Original confirmation issued at €1,205.50 · re-issue confirmation on save.</small>
</div>
</aside>
</div>
<footer class="d-foot">
<button class="ghost">Cancel reservation</button>
<div class="d-foot-right">
<button class="ghost">Send updated confirmation</button>
<button class="primary" id="save">Save changes</button>
</div>
</footer>
</section>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Reservation Edit
The dialog a receptionist or revenue manager opens to amend a confirmed booking. Two-column layout: left side has dates (arrival · departure with nights badge), room category, rate plan, guest count and special requests; the right side is a live rate summary that recalculates as the form changes (nights × ADR + city tax). Footer carries Cancel · Send confirmation · Save changes.