Coworking — Desk Booking
A warm industrial desk-booking interface for a coworking studio. An interactive floor plan colors each desk by live status (free, reserved, occupied), while a date row and time-slot picker drive availability. Selecting a desk opens a detail card with its type, amenities and price in dollars or credits, and a running summary totals the cost before you confirm. Confirming marks the desk occupied to mimic real-time updates, with a toast acknowledgement.
MCP
Code
:root {
--concrete: #efeae3;
--concrete-d: #e2dcd2;
--amber: #e8902b;
--amber-d: #cc7918;
--amber-50: #fdf1e2;
--char: #1c1b19;
--ink: #26241f;
--ink-2: #4a463e;
--muted: #7b766c;
--bg: #f6f3ee;
--surface: #ffffff;
--plant: #5f7a52;
--line: rgba(28, 27, 25, 0.1);
--line-2: rgba(28, 27, 25, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--occupied: #d4503e;
--free: #2f9e6f;
--reserved: #d98a2b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(28, 27, 25, 0.06), 0 4px 14px rgba(28, 27, 25, 0.05);
--sh-2: 0 8px 30px rgba(28, 27, 25, 0.12);
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
background-image: radial-gradient(
1200px 600px at 90% -10%,
var(--amber-50),
transparent 60%
),
radial-gradient(900px 500px at -10% 110%, #eef2ea, transparent 60%);
}
.app {
max-width: 1180px;
margin: 0 auto;
padding: 22px clamp(14px, 4vw, 30px) 48px;
}
/* ---------- topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
width: 38px;
height: 38px;
border-radius: 11px;
background: linear-gradient(140deg, var(--char), #3a3833);
position: relative;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.06);
}
.brand__mark::after {
content: "";
position: absolute;
inset: 12px;
border-radius: 4px;
background: linear-gradient(150deg, var(--amber), var(--amber-d));
}
.brand__txt {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.brand__txt strong {
font-weight: 800;
letter-spacing: -0.01em;
color: var(--char);
}
.brand__txt span {
font-size: 12.5px;
color: var(--muted);
}
.topbar__right {
display: flex;
align-items: center;
gap: 16px;
}
.credits {
display: flex;
align-items: center;
gap: 9px;
padding: 7px 12px;
background: var(--concrete);
border: 1px solid var(--line);
border-radius: 999px;
}
.credits__label {
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
}
.credits__bar {
width: 84px;
height: 7px;
border-radius: 999px;
background: var(--concrete-d);
overflow: hidden;
box-shadow: inset 0 0 0 1px var(--line);
}
.credits__bar i {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--amber), var(--amber-d));
}
.credits__val {
font-size: 12.5px;
font-weight: 700;
color: var(--char);
}
.credits__val em {
font-style: normal;
font-weight: 500;
color: var(--muted);
}
.me__avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: #fff;
background: linear-gradient(140deg, var(--a), var(--b));
box-shadow: var(--sh-1);
}
/* ---------- layout ---------- */
.layout {
margin-top: 18px;
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 18px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 20px;
}
.panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 16px;
flex-wrap: wrap;
}
h1 {
margin: 0;
font-size: 21px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--char);
}
.sub {
margin: 3px 0 0;
font-size: 13px;
color: var(--muted);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
list-style: none;
margin: 0;
padding: 0;
font-size: 12px;
color: var(--ink-2);
}
.legend li {
display: inline-flex;
align-items: center;
gap: 6px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.dot--free {
background: var(--free);
}
.dot--reserved {
background: var(--reserved);
}
.dot--occupied {
background: var(--occupied);
}
.dot--sel {
background: var(--char);
}
/* ---------- floor plan ---------- */
.floor {
position: relative;
border: 1px dashed var(--line-2);
border-radius: var(--r-md);
background: repeating-linear-gradient(
0deg,
transparent 0 31px,
rgba(28, 27, 25, 0.04) 31px 32px
),
repeating-linear-gradient(
90deg,
transparent 0 31px,
rgba(28, 27, 25, 0.04) 31px 32px
),
var(--concrete);
padding: 46px 16px 18px;
min-height: 360px;
}
.floor__feature {
position: absolute;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.floor__feature--window {
top: 14px;
left: 16px;
right: 16px;
text-align: center;
padding-bottom: 8px;
border-bottom: 3px solid var(--amber);
color: var(--amber-d);
}
.floor__feature--phone {
bottom: 12px;
right: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 5px 9px;
}
.floor__plant {
position: absolute;
font-size: 22px;
filter: saturate(0.9);
}
.floor__plant--a {
bottom: 14px;
left: 18px;
}
.floor__plant--b {
top: 50px;
right: 20px;
}
.floor__grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
position: relative;
z-index: 1;
}
.desk {
position: relative;
aspect-ratio: 1 / 0.84;
border: 1.5px solid var(--line-2);
border-radius: 10px;
background: var(--surface);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
cursor: pointer;
font: inherit;
color: var(--ink);
transition: transform 0.12s ease, box-shadow 0.16s ease,
border-color 0.16s ease;
}
.desk::before {
content: "";
position: absolute;
top: 7px;
left: 7px;
width: 9px;
height: 9px;
border-radius: 3px;
background: var(--st);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--st) 22%, transparent);
}
.desk__id {
font-size: 12.5px;
font-weight: 700;
color: var(--char);
}
.desk__type {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.desk--free {
--st: var(--free);
border-color: color-mix(in srgb, var(--free) 45%, var(--line-2));
background: color-mix(in srgb, var(--free) 7%, var(--surface));
}
.desk--reserved {
--st: var(--reserved);
border-color: color-mix(in srgb, var(--reserved) 45%, var(--line-2));
background: color-mix(in srgb, var(--reserved) 9%, var(--surface));
}
.desk--occupied {
--st: var(--occupied);
border-color: color-mix(in srgb, var(--occupied) 40%, var(--line-2));
background: color-mix(in srgb, var(--occupied) 9%, var(--surface));
cursor: not-allowed;
opacity: 0.72;
}
.desk--free:hover,
.desk--reserved:hover {
transform: translateY(-2px);
box-shadow: var(--sh-2);
border-color: var(--char);
}
.desk:focus-visible {
outline: 3px solid color-mix(in srgb, var(--amber) 60%, transparent);
outline-offset: 2px;
}
.desk.is-selected {
--st: var(--char);
border-color: var(--char);
background: var(--char);
box-shadow: var(--sh-2);
}
.desk.is-selected .desk__id {
color: #fff;
}
.desk.is-selected .desk__type {
color: var(--amber);
}
.desk__fav {
position: absolute;
bottom: 6px;
right: 7px;
font-size: 10px;
color: var(--amber-d);
}
/* ---------- side ---------- */
.side {
display: flex;
flex-direction: column;
gap: 18px;
position: sticky;
top: 18px;
}
.side__block {
display: flex;
flex-direction: column;
gap: 9px;
}
.side__h {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.dates {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.date {
border: 1px solid var(--line);
background: var(--surface);
border-radius: var(--r-sm);
padding: 8px 4px;
text-align: center;
cursor: pointer;
font: inherit;
transition: border-color 0.15s ease, background 0.15s ease;
}
.date small {
display: block;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.date b {
display: block;
font-size: 16px;
font-weight: 800;
color: var(--char);
margin-top: 2px;
}
.date:hover {
border-color: var(--line-2);
}
.date.is-active {
background: var(--amber-50);
border-color: var(--amber);
}
.date:focus-visible {
outline: 3px solid color-mix(in srgb, var(--amber) 60%, transparent);
outline-offset: 1px;
}
.slots {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.slot {
border: 1px solid var(--line);
background: var(--surface);
border-radius: var(--r-sm);
padding: 9px 10px;
text-align: left;
cursor: pointer;
font: inherit;
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
transition: border-color 0.15s ease, background 0.15s ease;
}
.slot b {
font-size: 13px;
font-weight: 700;
color: var(--char);
}
.slot span {
font-size: 11px;
color: var(--muted);
}
.slot:hover:not([disabled]) {
border-color: var(--line-2);
}
.slot.is-active {
background: var(--char);
border-color: var(--char);
}
.slot.is-active b,
.slot.is-active span {
color: #fff;
}
.slot[disabled] {
opacity: 0.45;
cursor: not-allowed;
}
.slot:focus-visible {
outline: 3px solid color-mix(in srgb, var(--amber) 60%, transparent);
outline-offset: 1px;
}
/* ---------- detail popover ---------- */
.detail {
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--concrete);
padding: 14px;
min-height: 130px;
}
.detail__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 8px 4px;
}
.detail__icon {
font-size: 26px;
}
.detail__card {
display: flex;
flex-direction: column;
gap: 10px;
}
.detail__top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.detail__name {
font-size: 15px;
font-weight: 800;
color: var(--char);
}
.detail__type {
font-size: 12px;
color: var(--ink-2);
margin-top: 1px;
}
.tag {
font-size: 11px;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
white-space: nowrap;
}
.tag--free {
background: color-mix(in srgb, var(--free) 16%, #fff);
color: #1d6f4d;
}
.tag--reserved {
background: color-mix(in srgb, var(--reserved) 18%, #fff);
color: #97601a;
}
.amen {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.amen li {
list-style: none;
font-size: 11.5px;
font-weight: 600;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 9px;
}
.detail__price {
display: flex;
align-items: baseline;
justify-content: space-between;
border-top: 1px dashed var(--line-2);
padding-top: 9px;
}
.detail__price b {
font-size: 19px;
font-weight: 800;
color: var(--char);
}
.detail__price span {
font-size: 12px;
color: var(--muted);
}
.detail__price .credits-cost {
font-size: 12.5px;
font-weight: 700;
color: var(--amber-d);
}
/* ---------- summary ---------- */
.summary {
border: 1px solid var(--char);
border-radius: var(--r-md);
background: linear-gradient(160deg, #fff, var(--amber-50));
padding: 16px;
box-shadow: var(--sh-1);
}
.summary__rows {
margin: 4px 0 14px;
}
.summary__rows > div {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
padding: 6px 0;
border-bottom: 1px solid var(--line);
}
.summary__rows dt {
font-size: 12.5px;
color: var(--muted);
}
.summary__rows dd {
margin: 0;
font-size: 13.5px;
font-weight: 600;
color: var(--ink);
text-align: right;
}
.summary__total {
border-bottom: none !important;
padding-top: 10px !important;
}
.summary__total dt {
font-weight: 700;
color: var(--ink) !important;
font-size: 14px !important;
}
.summary__total dd {
font-size: 18px !important;
font-weight: 800 !important;
color: var(--char) !important;
}
.btn {
font: inherit;
border: none;
cursor: pointer;
border-radius: var(--r-sm);
font-weight: 700;
transition: transform 0.1s ease, filter 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--primary {
width: 100%;
padding: 13px;
font-size: 15px;
color: #fff;
background: linear-gradient(140deg, var(--amber), var(--amber-d));
box-shadow: 0 6px 16px rgba(204, 121, 24, 0.32);
}
.btn--primary:hover {
filter: brightness(1.05);
}
.btn--primary:focus-visible {
outline: 3px solid var(--char);
outline-offset: 2px;
}
.btn--primary:disabled {
background: var(--concrete-d);
color: var(--muted);
box-shadow: none;
cursor: not-allowed;
}
.summary__note {
margin: 10px 0 0;
font-size: 11.5px;
text-align: center;
color: var(--muted);
}
/* ---------- toast ---------- */
.toast-wrap {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
width: min(92vw, 380px);
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
background: var(--char);
color: #fff;
border-radius: var(--r-md);
box-shadow: var(--sh-2);
font-size: 13.5px;
font-weight: 500;
animation: toast-in 0.28s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.toast::before {
content: "✓";
display: grid;
place-items: center;
width: 20px;
height: 20px;
flex: none;
border-radius: 50%;
background: var(--ok);
font-size: 12px;
font-weight: 800;
}
.toast.is-out {
animation: toast-out 0.25s ease forwards;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(12px) scale(0.96);
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(8px) scale(0.97);
}
}
/* ---------- responsive ---------- */
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.side {
position: static;
}
}
@media (max-width: 520px) {
.app {
padding: 14px 12px 40px;
}
.topbar {
flex-wrap: wrap;
}
.credits__bar {
width: 60px;
}
.floor__grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.floor {
padding: 42px 12px 16px;
}
.dates {
grid-template-columns: repeat(4, 1fr);
}
h1 {
font-size: 18px;
}
.floor__plant--b,
.floor__feature--phone {
display: none;
}
}(function () {
"use strict";
// ---- fictional data ---------------------------------------------------
var DESKS = [
{ id: "A1", type: "Hot desk", status: "free", price: 14, credits: 2, fav: false, amenities: ["Monitor", "Power", "Window seat"] },
{ id: "A2", type: "Hot desk", status: "occupied", price: 14, credits: 2, fav: false, amenities: ["Power", "Window seat"] },
{ id: "A3", type: "Hot desk", status: "free", price: 14, credits: 2, fav: true, amenities: ["Monitor", "Power", "Standing"] },
{ id: "A4", type: "Quiet desk", status: "reserved", price: 18, credits: 3, fav: false, amenities: ["Monitor", "Power", "Acoustic"] },
{ id: "A5", type: "Quiet desk", status: "free", price: 18, credits: 3, fav: false, amenities: ["Dual monitor", "Power", "Acoustic"] },
{ id: "B1", type: "Hot desk", status: "free", price: 14, credits: 2, fav: false, amenities: ["Power", "Plant view"] },
{ id: "B2", type: "Standing", status: "occupied", price: 16, credits: 2, fav: false, amenities: ["Standing", "Power", "Monitor"] },
{ id: "B3", type: "Standing", status: "free", price: 16, credits: 2, fav: true, amenities: ["Standing", "Power", "Dual monitor"] },
{ id: "B4", type: "Quiet desk", status: "free", price: 18, credits: 3, fav: false, amenities: ["Monitor", "Acoustic", "Locker"] },
{ id: "B5", type: "Hot desk", status: "occupied", price: 14, credits: 2, fav: false, amenities: ["Power", "Monitor"] },
{ id: "C1", type: "Studio bench", status: "reserved", price: 22, credits: 4, fav: false, amenities: ["Wide bench", "Power", "Daylight lamp"] },
{ id: "C2", type: "Studio bench", status: "free", price: 22, credits: 4, fav: true, amenities: ["Wide bench", "Power", "Daylight lamp", "Storage"] },
{ id: "C3", type: "Hot desk", status: "free", price: 14, credits: 2, fav: false, amenities: ["Power", "Plant view"] },
{ id: "C4", type: "Quiet desk", status: "free", price: 18, credits: 3, fav: false, amenities: ["Monitor", "Acoustic"] },
];
var SLOTS = [
{ id: "am", label: "Morning", time: "08–12", hours: 4 },
{ id: "pm", label: "Afternoon", time: "12–18", hours: 6 },
{ id: "eve", label: "Evening", time: "18–22", hours: 4 },
{ id: "day", label: "Full day", time: "08–22", hours: 14 },
];
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var MON_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var state = { deskId: null, dateIdx: 0, slotId: null };
var grid = document.getElementById("deskGrid");
var dateRow = document.getElementById("dateRow");
var slotRow = document.getElementById("slotRow");
var detail = document.getElementById("deskDetail");
var summary = document.getElementById("summary");
// ---- toast ------------------------------------------------------------
function toast(msg) {
var wrap = document.getElementById("toastWrap");
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
wrap.appendChild(el);
setTimeout(function () {
el.classList.add("is-out");
setTimeout(function () {
el.remove();
}, 250);
}, 2600);
}
// ---- dates ------------------------------------------------------------
function buildDates() {
var base = new Date(2026, 5, 18); // fictional fixed "today"
for (var i = 0; i < 4; i++) {
var d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + i);
var btn = document.createElement("button");
btn.type = "button";
btn.className = "date" + (i === 0 ? " is-active" : "");
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", i === 0 ? "true" : "false");
btn.dataset.idx = String(i);
var label = i === 0 ? "Today" : DAY_NAMES[d.getDay()];
btn.innerHTML =
"<small>" + label + "</small><b>" + d.getDate() + "</b>";
btn.dataset.full =
DAY_NAMES[d.getDay()] + " " + MON_NAMES[d.getMonth()] + " " + d.getDate();
btn.addEventListener("click", function () {
state.dateIdx = parseInt(this.dataset.idx, 10);
[].forEach.call(dateRow.children, function (c) {
var on = c === this;
c.classList.toggle("is-active", on);
c.setAttribute("aria-checked", on ? "true" : "false");
}, this);
refresh();
});
dateRow.appendChild(btn);
}
}
// ---- slots ------------------------------------------------------------
function buildSlots() {
SLOTS.forEach(function (s) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "slot";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
btn.dataset.id = s.id;
btn.innerHTML =
"<b>" + s.label + "</b><span>" + s.time + "</span>";
btn.addEventListener("click", function () {
if (this.hasAttribute("disabled")) return;
state.slotId = s.id;
[].forEach.call(slotRow.children, function (c) {
var on = c === this;
c.classList.toggle("is-active", on);
c.setAttribute("aria-checked", on ? "true" : "false");
}, this);
refresh();
});
slotRow.appendChild(btn);
});
}
// ---- desks ------------------------------------------------------------
function buildDesks() {
DESKS.forEach(function (desk) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "desk desk--" + desk.status;
btn.dataset.id = desk.id;
var label =
"Desk " + desk.id + ", " + desk.type + ", " + statusWord(desk.status);
btn.setAttribute("aria-label", label);
if (desk.status === "occupied") btn.setAttribute("aria-disabled", "true");
btn.innerHTML =
'<span class="desk__id">' + desk.id + "</span>" +
'<span class="desk__type">' + desk.type + "</span>" +
(desk.fav ? '<span class="desk__fav" title="One of your favourites">★</span>' : "");
btn.addEventListener("click", function () {
selectDesk(desk.id);
});
grid.appendChild(btn);
});
}
function statusWord(s) {
if (s === "free") return "available";
if (s === "reserved") return "reserved, request only";
return "occupied";
}
function selectDesk(id) {
var desk = DESKS.find(function (d) { return d.id === id; });
if (!desk) return;
if (desk.status === "occupied") {
toast("Desk " + id + " is occupied — pick another.");
return;
}
state.deskId = id;
[].forEach.call(grid.children, function (c) {
c.classList.toggle("is-selected", c.dataset.id === id);
});
renderDetail(desk);
refresh();
}
function renderDetail(desk) {
var reserved = desk.status === "reserved";
var amen = desk.amenities
.map(function (a) { return "<li>" + a + "</li>"; })
.join("");
detail.innerHTML =
'<div class="detail__card">' +
'<div class="detail__top">' +
"<div>" +
'<div class="detail__name">Desk ' + desk.id + "</div>" +
'<div class="detail__type">' + desk.type + "</div>" +
"</div>" +
'<span class="tag tag--' + (reserved ? "reserved" : "free") + '">' +
(reserved ? "Request" : "Available") +
"</span>" +
"</div>" +
'<ul class="amen">' + amen + "</ul>" +
'<div class="detail__price">' +
"<div><b>$" + desk.price + "</b> <span>/ slot</span></div>" +
'<span class="credits-cost">or ' + desk.credits + " credits</span>" +
"</div>" +
"</div>";
}
// ---- summary / refresh ------------------------------------------------
function refresh() {
var desk = DESKS.find(function (d) { return d.id === state.deskId; });
var slot = SLOTS.find(function (s) { return s.id === state.slotId; });
var dateBtn = dateRow.children[state.dateIdx];
if (!desk) {
summary.hidden = true;
return;
}
summary.hidden = false;
document.getElementById("sumDesk").textContent =
desk.id + " · " + desk.type;
document.getElementById("sumWhen").textContent =
(dateBtn ? dateBtn.dataset.full : "—") +
(slot ? " · " + slot.label + " (" + slot.time + ")" : " · pick a slot");
document.getElementById("sumDur").textContent =
slot ? slot.hours + " hours" : "—";
var total = document.getElementById("sumTotal");
var btn = document.getElementById("confirmBtn");
if (slot) {
// full day priced as ~3 slots for the discount feel
var mult = slot.id === "day" ? 2.6 : slot.hours <= 4 ? 1 : 1.5;
var cost = Math.round(desk.price * mult);
var cr = Math.round(desk.credits * mult);
total.textContent = "$" + cost + " · " + cr + " credits";
btn.disabled = false;
} else {
total.textContent = "—";
btn.disabled = true;
}
}
// ---- confirm ----------------------------------------------------------
document.getElementById("confirmBtn").addEventListener("click", function () {
var desk = DESKS.find(function (d) { return d.id === state.deskId; });
var slot = SLOTS.find(function (s) { return s.id === state.slotId; });
if (!desk || !slot) return;
var dateBtn = dateRow.children[state.dateIdx];
toast(
"Desk " + desk.id + " booked · " +
(dateBtn ? dateBtn.dataset.full : "") + " · " + slot.label
);
// mark desk occupied to simulate live availability
desk.status = "occupied";
var node = grid.querySelector('[data-id="' + desk.id + '"]');
if (node) {
node.className = "desk desk--occupied";
node.setAttribute("aria-disabled", "true");
}
state.deskId = null;
state.slotId = null;
[].forEach.call(slotRow.children, function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-checked", "false");
});
detail.innerHTML =
'<div class="detail__empty"><span class="detail__icon">🪑</span>' +
"<p>Select a desk on the plan to see its type, amenities and price.</p></div>";
summary.hidden = true;
});
// ---- init -------------------------------------------------------------
buildDates();
buildSlots();
buildDesks();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coworking — Desk Booking</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true"></span>
<div class="brand__txt">
<strong>Foundry & Field</strong>
<span>Warehouse District · Floor 2</span>
</div>
</div>
<div class="topbar__right">
<div class="credits" title="Booking credits remaining this month">
<span class="credits__label">Credits</span>
<span class="credits__bar"><i style="width:64%"></i></span>
<span class="credits__val">32 <em>/ 50</em></span>
</div>
<div class="me" title="Signed in as Mara Velez">
<span class="me__avatar" style="--a:#e8902b;--b:#cc7918">MV</span>
</div>
</div>
</header>
<main class="layout">
<section class="panel plan" aria-label="Floor plan">
<div class="panel__head">
<div>
<h1>Pick a desk</h1>
<p class="sub">East studio · 14 desks · live availability</p>
</div>
<ul class="legend" aria-hidden="true">
<li><span class="dot dot--free"></span>Free</li>
<li><span class="dot dot--reserved"></span>Reserved</li>
<li><span class="dot dot--occupied"></span>Occupied</li>
<li><span class="dot dot--sel"></span>Selected</li>
</ul>
</div>
<div class="floor" role="group" aria-label="Desk map">
<div class="floor__feature floor__feature--window" aria-hidden="true">Windows · east light</div>
<div class="floor__plant floor__plant--a" aria-hidden="true">🪴</div>
<div class="floor__plant floor__plant--b" aria-hidden="true">🌿</div>
<div class="floor__feature floor__feature--phone" aria-hidden="true">Phone booths</div>
<div class="floor__grid" id="deskGrid"><!-- desks injected by JS --></div>
</div>
</section>
<aside class="panel side" aria-label="Booking details">
<div class="side__block">
<h2 class="side__h">Date</h2>
<div class="dates" id="dateRow" role="radiogroup" aria-label="Choose a date"></div>
</div>
<div class="side__block">
<h2 class="side__h">Time slot</h2>
<div class="slots" id="slotRow" role="radiogroup" aria-label="Choose a time slot"></div>
</div>
<div class="side__block detail" id="deskDetail" aria-live="polite">
<div class="detail__empty" id="detailEmpty">
<span class="detail__icon" aria-hidden="true">🪑</span>
<p>Select a desk on the plan to see its type, amenities and price.</p>
</div>
</div>
<div class="summary" id="summary" hidden>
<h2 class="side__h">Summary</h2>
<dl class="summary__rows">
<div><dt>Desk</dt><dd id="sumDesk">—</dd></div>
<div><dt>When</dt><dd id="sumWhen">—</dd></div>
<div><dt>Duration</dt><dd id="sumDur">—</dd></div>
<div class="summary__total"><dt>Total</dt><dd id="sumTotal">—</dd></div>
</dl>
<button class="btn btn--primary" id="confirmBtn" type="button">
Confirm booking
</button>
<p class="summary__note">You can cancel free up to 2 hours before.</p>
</div>
</aside>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Desk Booking
A self-contained desk-booking flow for a fictional coworking studio, Foundry & Field. The left panel renders a warm-concrete floor plan: fourteen desks laid out on a grid, each tinted by its live status — green for free, amber for reserved, red for occupied — with plant accents, window seating and phone-booth markers for context. Occupied desks are non-interactive and announce themselves to assistive tech; free and reserved desks lift on hover and snap to a matte black selected state.
The right rail handles the booking itself. A four-day date row and a four-option time-slot picker (morning, afternoon, evening or full day) act as keyboard-usable radio groups. Picking a desk opens a detail card listing its type, amenities and price in both dollars and credits. As soon as a slot is chosen, the summary panel totals duration and cost, and the confirm button enables.
Confirming fires a toast, flips the booked desk to occupied right on the plan to simulate live availability, and resets the selection — all in vanilla JavaScript with no frameworks or build step. The amber-on-concrete palette, Inter type and soft shadows give it a community-studio feel, and the layout reflows to a single column down to roughly 360px wide.
Illustrative UI only — fictional coworking space, not a real booking system.