Coworking — Live Floor Plan
A warm industrial live floor plan for a fictional coworking space. Desks and meeting rooms are laid out across four zones and colored by real-time status — free, in use, or reserved — over a soft concrete grid. A capacity header tracks live occupancy, zone bars show pressure per area, and hovering any seat reveals who is there and until when. Zoom, filter by zone or status, watch seats flip on a live tick, and reserve open spots.
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: #e8902b;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-s: 0 1px 2px rgba(28, 27, 25, 0.06), 0 2px 8px rgba(28, 27, 25, 0.05);
--shadow-m: 0 6px 24px rgba(28, 27, 25, 0.1);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
max-width: 1160px;
margin: 0 auto;
padding: 20px clamp(12px, 3vw, 28px) 48px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: space-between;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 20px;
box-shadow: var(--shadow-s);
}
.brand { display: flex; align-items: center; gap: 14px; }
.logo {
width: 46px; height: 46px;
border-radius: 12px;
background: linear-gradient(140deg, var(--char), #34322d);
display: grid; place-items: center;
font-size: 22px;
box-shadow: inset 0 0 0 2px rgba(232, 144, 43, 0.4);
}
.brand-txt h1 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -0.02em; color: var(--char); }
.brand-txt p { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); font-weight: 500; }
.capacity { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.cap-block { min-width: 220px; }
.cap-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); font-weight: 600; }
.cap-meter {
height: 10px;
background: var(--concrete-d);
border-radius: 999px;
overflow: hidden;
margin: 7px 0 6px;
box-shadow: inset 0 1px 2px rgba(28, 27, 25, 0.12);
}
.cap-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--plant), var(--amber) 60%, var(--danger));
transition: width 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.cap-readout { display: flex; align-items: baseline; gap: 8px; }
.cap-readout strong { font-size: 20px; font-weight: 800; color: var(--char); }
.cap-readout span { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.cap-pills { display: flex; flex-direction: column; gap: 6px; }
.pill {
display: inline-flex; align-items: center; gap: 7px;
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
}
.pill span:not(.dot) { color: var(--char); }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; }
.dot.free { background: var(--free); }
.dot.occupied { background: var(--occupied); }
.dot.reserved { background: var(--reserved); }
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
margin: 18px 0;
}
.filters { display: inline-flex; gap: 6px; background: var(--surface); padding: 5px; border-radius: 999px; border: 1px solid var(--line); box-shadow: var(--shadow-s); }
.chip {
border: none;
background: transparent;
padding: 8px 14px;
border-radius: 999px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
transition: background 0.15s, color 0.15s;
}
.chip:hover { background: var(--concrete); }
.chip.is-active { background: var(--char); color: #fff; }
.chip:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.zone-filter { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.zone-filter select {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--char);
padding: 9px 30px 9px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237b766c' stroke-width='3'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center;
-webkit-appearance: none; appearance: none;
cursor: pointer;
}
.zone-filter select:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
.zoom-ctl {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
box-shadow: var(--shadow-s);
}
.zoom-ctl button {
border: none; background: transparent; width: 32px; height: 32px;
border-radius: 999px; font: inherit; font-size: 18px; font-weight: 700;
color: var(--char); cursor: pointer; display: grid; place-items: center;
transition: background 0.15s;
}
.zoom-ctl button.ghost { width: auto; padding: 0 12px; font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.zoom-ctl button:hover { background: var(--concrete); }
.zoom-ctl button:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
#zoomLabel { font-size: 12.5px; font-weight: 700; color: var(--ink-2); min-width: 42px; text-align: center; }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 18px;
align-items: start;
}
.plan-wrap {
position: relative;
background:
radial-gradient(circle at 20% 0%, rgba(232, 144, 43, 0.06), transparent 55%),
var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--shadow-s);
overflow: hidden;
}
.plan-stage {
overflow: auto;
border-radius: var(--r-md);
background:
linear-gradient(var(--line) 1px, transparent 1px) 0 0 / 28px 28px,
linear-gradient(90deg, var(--line) 1px, transparent 1px) 0 0 / 28px 28px,
var(--concrete);
padding: 18px;
min-height: 420px;
scrollbar-width: thin;
}
.plan {
width: 640px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
transform-origin: top center;
transition: transform 0.25s ease;
}
.zone {
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 12px;
position: relative;
transition: opacity 0.2s, filter 0.2s;
}
.zone.dim { opacity: 0.32; filter: grayscale(0.4); }
.zone-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.zone-head h4 { margin: 0; font-size: 12.5px; font-weight: 700; color: var(--char); letter-spacing: -0.01em; }
.zone-head .zone-occ { font-size: 11px; font-weight: 600; color: var(--muted); }
.seats { display: grid; gap: 8px; }
.seats.cols-3 { grid-template-columns: repeat(3, 1fr); }
.seats.cols-2 { grid-template-columns: repeat(2, 1fr); }
.seat {
--c: var(--free);
position: relative;
border: none;
font: inherit;
cursor: pointer;
aspect-ratio: 1 / 0.82;
border-radius: 9px;
background: color-mix(in srgb, var(--c) 16%, #fff);
box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--c) 55%, transparent);
display: grid;
place-items: center;
transition: transform 0.14s, box-shadow 0.14s, opacity 0.2s, filter 0.2s;
color: var(--ink-2);
font-size: 11px;
font-weight: 700;
}
.seat::after {
content: "";
position: absolute;
top: 6px; right: 6px;
width: 7px; height: 7px;
border-radius: 50%;
background: var(--c);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--c) 25%, #fff);
}
.seat[data-status="free"] { --c: var(--free); }
.seat[data-status="occupied"] { --c: var(--occupied); }
.seat[data-status="reserved"] { --c: var(--reserved); }
.seat.room { aspect-ratio: 1 / 0.5; grid-column: span 1; }
.seat:hover { transform: translateY(-2px) scale(1.04); box-shadow: inset 0 0 0 2px var(--c), var(--shadow-m); z-index: 2; }
.seat:focus-visible { outline: 2px solid var(--char); outline-offset: 2px; z-index: 2; }
.seat.is-selected { box-shadow: inset 0 0 0 2.5px var(--char), var(--shadow-m); transform: translateY(-2px) scale(1.04); }
.seat.hidden { opacity: 0.12; filter: grayscale(1); pointer-events: none; }
.seat.flash { animation: flash 0.7s ease; }
@keyframes flash {
0% { box-shadow: inset 0 0 0 2px var(--c), 0 0 0 0 color-mix(in srgb, var(--c) 50%, transparent); }
50% { box-shadow: inset 0 0 0 2px var(--c), 0 0 0 8px color-mix(in srgb, var(--c) 0%, transparent); }
100% { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--c) 55%, transparent); }
}
.live-tag {
position: absolute; top: 26px; right: 26px;
display: inline-flex; align-items: center; gap: 7px;
font-size: 11px; font-weight: 600; color: var(--ink-2);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(4px);
padding: 6px 11px; border-radius: 999px;
border: 1px solid var(--line);
}
.ping { width: 8px; height: 8px; border-radius: 50%; background: var(--plant); position: relative; }
.ping::before { content: ""; position: absolute; inset: 0; border-radius: 50%; background: var(--plant); animation: ping 1.8s ease-out infinite; }
@keyframes ping { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(3); opacity: 0; } }
.hint { margin-top: 12px; font-size: 11.5px; color: var(--muted); text-align: center; }
/* ---------- Side ---------- */
.side { display: grid; gap: 16px; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 18px;
box-shadow: var(--shadow-s);
}
.card h2 { margin: 0 0 12px; font-size: 13px; font-weight: 700; color: var(--char); letter-spacing: -0.01em; }
.zone-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 13px; }
.zone-row .zr-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 5px; }
.zone-row .zr-name { font-size: 13px; font-weight: 600; color: var(--ink); }
.zone-row .zr-val { font-size: 12px; font-weight: 700; color: var(--ink-2); }
.zr-bar { height: 8px; background: var(--concrete-d); border-radius: 999px; overflow: hidden; box-shadow: inset 0 1px 2px rgba(28,27,25,0.1); }
.zr-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--plant), var(--amber)); transition: width 0.8s cubic-bezier(0.22,0.61,0.36,1); }
.zr-fill.hot { background: linear-gradient(90deg, var(--amber), var(--danger)); }
.detail-empty { text-align: center; color: var(--muted); padding: 18px 8px; }
.detail-glyph { font-size: 30px; margin-bottom: 8px; }
.detail-empty p { margin: 0; font-size: 13px; }
.detail-head { margin-bottom: 14px; }
.detail-status {
display: inline-block; font-size: 10.5px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.06em; padding: 3px 9px; border-radius: 999px; margin-bottom: 8px;
color: #fff; background: var(--muted);
}
.detail-status.free { background: var(--free); }
.detail-status.occupied { background: var(--occupied); }
.detail-status.reserved { background: var(--reserved); color: var(--char); }
.detail-head h3 { margin: 0; font-size: 18px; font-weight: 800; color: var(--char); letter-spacing: -0.02em; }
.detail-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-top: 1px solid var(--line); font-size: 13px; }
.detail-row span { color: var(--muted); font-weight: 500; }
.detail-row strong { color: var(--ink); font-weight: 600; text-align: right; }
.book-btn {
width: 100%; margin-top: 14px; border: none; cursor: pointer;
font: inherit; font-size: 13.5px; font-weight: 700;
padding: 11px; border-radius: var(--r-sm);
background: var(--char); color: #fff;
transition: background 0.15s, transform 0.1s;
}
.book-btn:hover { background: var(--amber-d); }
.book-btn:active { transform: scale(0.98); }
.book-btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.book-btn:disabled { background: var(--concrete-d); color: var(--muted); cursor: not-allowed; }
/* ---------- Floating tip ---------- */
.floating-tip {
position: fixed;
z-index: 50;
pointer-events: none;
background: var(--char);
color: #fff;
padding: 8px 11px;
border-radius: var(--r-sm);
box-shadow: var(--shadow-m);
font-size: 12px;
max-width: 220px;
display: grid;
gap: 2px;
transform: translate(-50%, calc(-100% - 12px));
}
.floating-tip strong { font-weight: 700; font-size: 12.5px; }
.floating-tip span { color: rgba(255, 255, 255, 0.72); font-size: 11.5px; }
.floating-tip::after {
content: ""; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg);
width: 10px; height: 10px; background: var(--char);
}
/* ---------- Toast ---------- */
.toast-stack { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); display: grid; gap: 8px; z-index: 80; width: max-content; max-width: 92vw; }
.toast {
background: var(--char); color: #fff;
padding: 11px 16px; border-radius: var(--r-sm);
font-size: 13px; font-weight: 500;
box-shadow: var(--shadow-m);
display: flex; align-items: center; gap: 9px;
animation: toastIn 0.3s ease;
}
.toast .t-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--amber); flex: none; }
.toast.out { animation: toastOut 0.3s ease forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(12px); } }
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.layout { grid-template-columns: 1fr; }
.plan { width: 100%; min-width: 480px; }
}
@media (max-width: 520px) {
.app { padding: 14px 12px 40px; }
.topbar { padding: 14px; gap: 14px; }
.cap-block { min-width: 100%; }
.cap-pills { flex-direction: row; flex-wrap: wrap; }
.toolbar { gap: 10px; }
.zoom-ctl { margin-left: 0; }
.filters { flex-wrap: wrap; }
.live-tag { top: 22px; right: 18px; }
.plan { gap: 12px; }
}(function () {
"use strict";
/* ---------- Fictional space data ---------- */
var members = [
"Mara Osei", "Tomás Vidal", "Priya Nandakumar", "Felix Brandt", "Aiko Tanaka",
"Devon Marsh", "Lena Hofer", "Samuel Adeyemi", "Noor Haddad", "Rafa Quirós",
"Ingrid Sø", "Bao Nguyen", "Cleo Marchetti", "Jonah Reyes", "Wren Calloway",
"Yara Sabbagh", "Otto Lindqvist", "Hana Park", "Milo Fenwick", "Sofia Duarte"
];
var zonesDef = [
{ id: "commons", name: "The Commons", type: "Hot desk", cols: 3, count: 9 },
{ id: "focus", name: "Focus Wing", type: "Quiet desk", cols: 3, count: 9 },
{ id: "studio", name: "Maker Studio", type: "Studio bench", cols: 2, count: 6 },
{ id: "rooms", name: "Meeting Rooms", type: "Room", cols: 2, count: 4, room: true }
];
var roomNames = ["The Boiler Room", "Greenhouse", "Loft 2A", "The Annex"];
var statuses = ["free", "occupied", "reserved"];
function rand(n) { return Math.floor(Math.random() * n); }
function pick(arr) { return arr[rand(arr.length)]; }
function randStatus() {
var r = Math.random();
if (r < 0.5) return "occupied";
if (r < 0.72) return "reserved";
return "free";
}
function randUntil() {
var h = 9 + rand(9);
var m = pick([0, 15, 30, 45]);
var ap = h >= 12 ? "pm" : "am";
var hh = h > 12 ? h - 12 : h;
return hh + ":" + (m < 10 ? "0" + m : m) + " " + ap;
}
/* ---------- Build seat model ---------- */
var seats = [];
zonesDef.forEach(function (z) {
for (var i = 0; i < z.count; i++) {
var st = z.room ? pick(["free", "occupied", "reserved"]) : randStatus();
var label = z.room ? roomNames[i] : (z.name.split(" ")[1] || z.name).slice(0, 1).toUpperCase() + "-" + (i + 1);
seats.push({
id: z.id + "-" + i,
zone: z.id,
zoneName: z.name,
type: z.type,
room: !!z.room,
label: z.room ? roomNames[i] : label,
status: st,
member: st === "free" ? null : pick(members),
until: st === "free" ? null : randUntil()
});
}
});
/* ---------- Render plan ---------- */
var plan = document.getElementById("plan");
var seatEls = {};
zonesDef.forEach(function (z) {
var zEl = document.createElement("div");
zEl.className = "zone";
zEl.dataset.zone = z.id;
zEl.innerHTML =
'<div class="zone-head"><h4>' + z.name + '</h4>' +
'<span class="zone-occ" data-occ="' + z.id + '"></span></div>' +
'<div class="seats cols-' + z.cols + '" data-seats="' + z.id + '"></div>';
plan.appendChild(zEl);
var seatWrap = zEl.querySelector("[data-seats]");
seats.filter(function (s) { return s.zone === z.id; }).forEach(function (s) {
var btn = document.createElement("button");
btn.className = "seat" + (s.room ? " room" : "");
btn.dataset.status = s.status;
btn.dataset.id = s.id;
btn.type = "button";
btn.textContent = s.room ? "" : s.label;
btn.setAttribute("aria-label", "");
seatWrap.appendChild(btn);
seatEls[s.id] = btn;
updateAria(s);
});
});
function seatById(id) {
for (var i = 0; i < seats.length; i++) if (seats[i].id === id) return seats[i];
return null;
}
function statusLabel(st) { return st === "free" ? "Free" : st === "occupied" ? "In use" : "Reserved"; }
function updateAria(s) {
var el = seatEls[s.id];
if (!el) return;
var desc = s.label + ", " + statusLabel(s.status);
if (s.member) desc += ", " + s.member + " until " + s.until;
el.setAttribute("aria-label", desc);
}
/* ---------- Stats ---------- */
function recompute() {
var counts = { free: 0, occupied: 0, reserved: 0 };
var total = seats.length;
seats.forEach(function (s) { counts[s.status]++; });
var used = counts.occupied + counts.reserved;
var pct = Math.round((used / total) * 100);
document.getElementById("capFill").style.width = pct + "%";
document.getElementById("capPct").textContent = pct + "%";
document.getElementById("capCount").textContent = used + " / " + total + " seats";
document.getElementById("freeCount").textContent = counts.free;
document.getElementById("occCount").textContent = counts.occupied;
document.getElementById("resCount").textContent = counts.reserved;
zonesDef.forEach(function (z) {
var zSeats = seats.filter(function (s) { return s.zone === z.id; });
var zUsed = zSeats.filter(function (s) { return s.status !== "free"; }).length;
var zPct = Math.round((zUsed / zSeats.length) * 100);
var occEl = plan.querySelector('[data-occ="' + z.id + '"]');
if (occEl) occEl.textContent = zUsed + "/" + zSeats.length + " in use";
});
renderZoneList();
}
function renderZoneList() {
var list = document.getElementById("zoneList");
list.innerHTML = "";
zonesDef.forEach(function (z) {
var zSeats = seats.filter(function (s) { return s.zone === z.id; });
var zUsed = zSeats.filter(function (s) { return s.status !== "free"; }).length;
var zPct = Math.round((zUsed / zSeats.length) * 100);
var li = document.createElement("li");
li.className = "zone-row";
li.innerHTML =
'<div class="zr-top"><span class="zr-name">' + z.name + '</span>' +
'<span class="zr-val">' + zPct + '%</span></div>' +
'<div class="zr-bar"><div class="zr-fill' + (zPct >= 75 ? " hot" : "") + '" style="width:' + zPct + '%"></div></div>';
list.appendChild(li);
});
}
/* ---------- Detail panel ---------- */
var selectedId = null;
function showDetail(s) {
document.getElementById("detailEmpty").hidden = true;
var body = document.getElementById("detailBody");
body.hidden = false;
var statusEl = document.getElementById("detStatus");
statusEl.textContent = statusLabel(s.status);
statusEl.className = "detail-status " + s.status;
document.getElementById("detName").textContent = s.label;
document.getElementById("detZone").textContent = s.zoneName;
document.getElementById("detType").textContent = s.type;
var memberRow = document.getElementById("detMemberRow");
var untilRow = document.getElementById("detUntilRow");
if (s.member) {
memberRow.style.display = "";
untilRow.style.display = "";
document.getElementById("detMember").textContent = s.member;
document.getElementById("detUntil").textContent = s.until;
} else {
memberRow.style.display = "none";
untilRow.style.display = "none";
}
var btn = document.getElementById("bookBtn");
if (s.status === "free") {
btn.disabled = false;
btn.textContent = "Reserve " + s.label;
} else if (s.status === "reserved") {
btn.disabled = true;
btn.textContent = "Already reserved";
} else {
btn.disabled = true;
btn.textContent = "Occupied right now";
}
btn.dataset.id = s.id;
}
function selectSeat(id) {
if (selectedId && seatEls[selectedId]) seatEls[selectedId].classList.remove("is-selected");
selectedId = id;
var s = seatById(id);
if (seatEls[id]) seatEls[id].classList.add("is-selected");
showDetail(s);
}
document.getElementById("bookBtn").addEventListener("click", function () {
var s = seatById(this.dataset.id);
if (!s || s.status !== "free") return;
s.status = "reserved";
s.member = "You";
s.until = randUntil();
seatEls[s.id].dataset.status = "reserved";
seatEls[s.id].classList.add("flash");
setTimeout(function () { seatEls[s.id].classList.remove("flash"); }, 700);
updateAria(s);
recompute();
showDetail(s);
applyFilters();
toast(s.label + " reserved · held for you until " + s.until);
});
/* ---------- Floating tip ---------- */
var tip = document.getElementById("tip");
function positionTip(e, s) {
document.getElementById("tipName").textContent = s.label + " · " + statusLabel(s.status);
var meta = s.member ? s.member + " until " + s.until : s.type + " · available now";
document.getElementById("tipMeta").textContent = meta;
tip.hidden = false;
var r = e.currentTarget.getBoundingClientRect();
tip.style.left = (r.left + r.width / 2) + "px";
tip.style.top = r.top + "px";
}
/* ---------- Wire seat events ---------- */
Object.keys(seatEls).forEach(function (id) {
var el = seatEls[id];
el.addEventListener("click", function () { selectSeat(id); });
el.addEventListener("mouseenter", function (e) { positionTip(e, seatById(id)); });
el.addEventListener("mouseleave", function () { tip.hidden = true; });
el.addEventListener("focus", function (e) { positionTip(e, seatById(id)); });
el.addEventListener("blur", function () { tip.hidden = true; });
});
/* ---------- Filters ---------- */
var activeStatus = "all";
var activeZone = "all";
function applyFilters() {
seats.forEach(function (s) {
var el = seatEls[s.id];
var statusOk = activeStatus === "all" || s.status === activeStatus;
var zoneOk = activeZone === "all" || s.zone === activeZone;
el.classList.toggle("hidden", !(statusOk && zoneOk));
});
document.querySelectorAll(".zone").forEach(function (z) {
z.classList.toggle("dim", activeZone !== "all" && z.dataset.zone !== activeZone);
});
}
document.querySelectorAll(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
document.querySelectorAll(".chip").forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
activeStatus = chip.dataset.status;
applyFilters();
});
});
document.getElementById("zoneSel").addEventListener("change", function () {
activeZone = this.value;
applyFilters();
});
/* ---------- Zoom ---------- */
var zoom = 1;
var ZMIN = 0.7, ZMAX = 1.6;
function applyZoom() {
plan.style.transform = "scale(" + zoom + ")";
document.getElementById("zoomLabel").textContent = Math.round(zoom * 100) + "%";
}
function setZoom(z) { zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(z * 100) / 100)); applyZoom(); }
document.getElementById("zoomIn").addEventListener("click", function () { setZoom(zoom + 0.15); });
document.getElementById("zoomOut").addEventListener("click", function () { setZoom(zoom - 0.15); });
document.getElementById("zoomReset").addEventListener("click", function () { setZoom(1); });
document.getElementById("planStage").addEventListener("wheel", function (e) {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
setZoom(zoom + (e.deltaY < 0 ? 0.12 : -0.12));
}, { passive: false });
/* ---------- Live simulation ---------- */
function tick() {
var movable = seats.filter(function (s) { return s.member !== "You"; });
var changes = 1 + rand(2);
for (var i = 0; i < changes; i++) {
var s = pick(movable);
var prev = s.status;
var next = pick(statuses.filter(function (x) { return x !== prev; }));
s.status = next;
if (next === "free") { s.member = null; s.until = null; }
else { s.member = pick(members); s.until = randUntil(); }
var el = seatEls[s.id];
el.dataset.status = next;
el.classList.add("flash");
(function (e) { setTimeout(function () { e.classList.remove("flash"); }, 700); })(el);
updateAria(s);
if (s.id === selectedId) showDetail(s);
}
recompute();
applyFilters();
}
/* ---------- Toast ---------- */
var stack = document.getElementById("toastStack");
function toast(msg) {
var t = document.createElement("div");
t.className = "toast";
t.innerHTML = '<span class="t-dot"></span><span></span>';
t.querySelector("span:last-child").innerHTML = msg;
stack.appendChild(t);
setTimeout(function () {
t.classList.add("out");
setTimeout(function () { t.remove(); }, 320);
}, 3200);
}
/* ---------- Init ---------- */
recompute();
applyFilters();
applyZoom();
setTimeout(function () { tick(); }, 2500);
setInterval(tick, 4200);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loftworks Studio — Live Floor Plan</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">
<div class="logo" aria-hidden="true">
<span class="leaf">🌿</span>
</div>
<div class="brand-txt">
<h1>Loftworks Studio</h1>
<p>Warehouse District · Floor 2</p>
</div>
</div>
<div class="capacity" role="group" aria-label="Live capacity">
<div class="cap-block">
<span class="cap-label">Live occupancy</span>
<div class="cap-meter">
<div class="cap-fill" id="capFill" style="width:0%"></div>
</div>
<div class="cap-readout">
<strong id="capPct">0%</strong>
<span id="capCount">0 / 0 seats</span>
</div>
</div>
<div class="cap-pills">
<div class="pill"><span class="dot free"></span><span id="freeCount">0</span> free</div>
<div class="pill"><span class="dot occupied"></span><span id="occCount">0</span> in use</div>
<div class="pill"><span class="dot reserved"></span><span id="resCount">0</span> reserved</div>
</div>
</div>
</header>
<div class="toolbar">
<div class="filters" role="group" aria-label="Filter by status">
<button class="chip is-active" data-status="all" aria-pressed="true">All</button>
<button class="chip" data-status="free" aria-pressed="false"><span class="dot free"></span>Free</button>
<button class="chip" data-status="occupied" aria-pressed="false"><span class="dot occupied"></span>In use</button>
<button class="chip" data-status="reserved" aria-pressed="false"><span class="dot reserved"></span>Reserved</button>
</div>
<div class="zone-filter">
<label for="zoneSel">Zone</label>
<select id="zoneSel" aria-label="Filter by zone">
<option value="all">All zones</option>
<option value="commons">The Commons</option>
<option value="focus">Focus Wing</option>
<option value="studio">Maker Studio</option>
<option value="rooms">Meeting Rooms</option>
</select>
</div>
<div class="zoom-ctl" role="group" aria-label="Zoom controls">
<button id="zoomOut" aria-label="Zoom out">−</button>
<span id="zoomLabel">100%</span>
<button id="zoomIn" aria-label="Zoom in">+</button>
<button id="zoomReset" class="ghost">Reset</button>
</div>
</div>
<main class="layout">
<section class="plan-wrap" aria-label="Floor plan">
<div class="plan-stage" id="planStage">
<div class="plan" id="plan">
<!-- zones rendered by JS -->
</div>
</div>
<div class="live-tag"><span class="ping"></span>Live · updates every few seconds</div>
<div class="hint">Hover a seat for details · scroll or use + / − to zoom</div>
</section>
<aside class="side">
<div class="card zones-card">
<h2>Zone occupancy</h2>
<ul class="zone-list" id="zoneList"></ul>
</div>
<div class="card detail-card" id="detailCard">
<div class="detail-empty" id="detailEmpty">
<div class="detail-glyph" aria-hidden="true">📍</div>
<p>Hover or tap a seat on the plan to see who's there and until when.</p>
</div>
<div class="detail-body" id="detailBody" hidden>
<div class="detail-head">
<span class="detail-status" id="detStatus">Free</span>
<h3 id="detName">Desk</h3>
</div>
<div class="detail-row"><span>Zone</span><strong id="detZone">—</strong></div>
<div class="detail-row" id="detMemberRow"><span>Member</span><strong id="detMember">—</strong></div>
<div class="detail-row" id="detUntilRow"><span>Until</span><strong id="detUntil">—</strong></div>
<div class="detail-row"><span>Type</span><strong id="detType">—</strong></div>
<button class="book-btn" id="bookBtn">Reserve this seat</button>
</div>
</div>
</aside>
</main>
</div>
<div class="floating-tip" id="tip" hidden>
<strong id="tipName"></strong>
<span id="tipMeta"></span>
</div>
<div class="toast-stack" id="toastStack" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Live Floor Plan
A community-ops view of Loftworks Studio, a fictional warehouse coworking space. The plan arranges hot desks, quiet desks, studio benches and meeting rooms across four labeled zones, each seat tinted by its current status: green for free, red for in use, amber for reserved. A capacity header sums it all into a single live occupancy percentage with a gradient meter, free / in-use / reserved pills, and a per-zone occupancy list with bars that turn hot above 75 percent.
The plan is built to be poked at. Hover or focus any seat for a floating tooltip with the member name and the time their booking runs until; click to pin a full detail card on the side. Filter the view by status chips or by zone, and the off-target seats fade while the selected zone stays sharp. Zoom in and out with the controls or Ctrl/Cmd-scroll to inspect a busy corner.
Everything updates on a live tick: every few seconds a handful of seats flip status with a ripple animation, the capacity meter eases to its new value, and zone bars re-balance. Open seats can be reserved straight from the detail card, which locks them in and fires a toast — your own reservation is protected from the simulation so it stays held.
Illustrative UI only — fictional coworking space, not a real booking system.