Pages Hard
Reservation Timeline
Gantt-style reservation timeline: tables as rows, time as columns, bookings as colored blocks with guest name and party size — hover for details, click to edit status.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* ============================================================
REST-RESERV-TIMELINE — Phase 27 Restaurant Theme
============================================================ */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--red-marker: #E53E3E;
--font-heading: 'Playfair Display', Georgia, serif;
--font-body: 'Inter', system-ui, sans-serif;
--col-width: 110px;
--row-height: 52px;
--label-width: 80px;
--header-h: 64px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--cream);
color: var(--ink);
font-family: var(--font-body);
font-size: 14px;
}
/* ── HEADER ──────────────────────────────────────────────── */
.header {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-h);
padding: 0 24px;
background: var(--forest-d);
border-bottom: 2px solid var(--forest);
gap: 16px;
}
.header-left {
display: flex;
align-items: baseline;
gap: 16px;
}
.header-title {
font-family: var(--font-heading);
font-size: 1.35rem;
font-weight: 800;
color: var(--bone);
white-space: nowrap;
}
.header-date {
font-size: 0.82rem;
font-weight: 500;
color: var(--warm-gray);
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
flex-shrink: 0;
}
/* Legend chips */
.legend {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.legend-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
white-space: nowrap;
}
.legend-chip.confirmed { background: var(--forest); color: var(--bone); }
.legend-chip.pending { background: var(--gold); color: var(--ink); }
.legend-chip.seated { background: var(--terracotta); color: var(--bone); }
.legend-chip.done { background: var(--warm-gray); color: var(--bone); }
/* New reservation button */
.btn-new {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 18px;
height: 38px;
background: var(--terracotta);
color: #fff;
border: none;
border-radius: 8px;
font-family: var(--font-body);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.btn-new:hover { background: #a84e22; }
/* ── TIMELINE WRAPPER ────────────────────────────────────── */
.timeline-wrapper {
padding: 20px 24px 40px;
overflow: hidden;
}
.timeline-scroll {
display: grid;
/* col 0 = table labels (fixed), col 1..N = time slots */
grid-template-columns: var(--label-width) repeat(10, var(--col-width));
grid-template-rows: 36px repeat(16, var(--row-height));
position: relative;
border: 1px solid var(--bone);
border-radius: 10px;
overflow-x: auto;
background: #fff;
box-shadow: 0 2px 12px rgba(44,26,14,0.07);
}
/* Corner cell */
.corner-cell {
grid-column: 1;
grid-row: 1;
position: sticky;
left: 0;
z-index: 10;
background: var(--bone);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warm-gray);
border-right: 2px solid var(--bone);
border-bottom: 2px solid var(--bone);
}
/* Time header */
.time-header {
grid-column: 2 / -1;
grid-row: 1;
display: flex;
position: sticky;
top: 0;
z-index: 8;
background: var(--bone);
border-bottom: 2px solid var(--bone);
}
.time-cell {
width: var(--col-width);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.73rem;
font-weight: 600;
color: var(--warm-gray);
border-right: 1px solid #ddd8cf;
}
.time-cell:last-child { border-right: none; }
/* Table labels column */
.table-labels {
grid-column: 1;
grid-row: 2 / -1;
position: sticky;
left: 0;
z-index: 6;
background: var(--cream);
border-right: 2px solid var(--bone);
display: flex;
flex-direction: column;
}
.table-label-cell {
height: var(--row-height);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
font-weight: 700;
color: var(--ink);
border-bottom: 1px solid var(--bone);
padding: 0 6px;
text-align: center;
}
.table-label-cell:last-child { border-bottom: none; }
/* Grid body */
.grid-body {
grid-column: 2 / -1;
grid-row: 2 / -1;
position: relative;
}
.grid-row {
display: flex;
height: var(--row-height);
position: relative;
border-bottom: 1px solid var(--bone);
}
.grid-row:last-child { border-bottom: none; }
.grid-cell {
width: var(--col-width);
flex-shrink: 0;
border-right: 1px solid var(--bone);
height: 100%;
transition: background 0.1s;
}
.grid-cell:last-child { border-right: none; }
.grid-row:hover .grid-cell { background: rgba(52,95,64,0.03); }
/* ── RESERVATION BLOCKS ──────────────────────────────────── */
.reservation-block {
position: absolute;
top: 5px;
border-radius: 6px;
padding: 4px 8px;
overflow: hidden;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1px;
transition: filter 0.12s, transform 0.12s;
z-index: 4;
min-width: 40px;
height: calc(var(--row-height) - 10px);
user-select: none;
}
.reservation-block:hover {
filter: brightness(1.06);
transform: translateY(-1px);
z-index: 5;
}
.reservation-block.confirmed { background: var(--forest); color: var(--bone); }
.reservation-block.pending { background: var(--gold); color: var(--ink); }
.reservation-block.seated { background: var(--terracotta); color: var(--bone); }
.reservation-block.done { background: var(--warm-gray); color: var(--bone); opacity: 0.75; }
.block-guest {
font-size: 0.72rem;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.block-pax {
font-size: 0.63rem;
font-weight: 500;
opacity: 0.85;
white-space: nowrap;
overflow: hidden;
}
/* ── TIME MARKER ─────────────────────────────────────────── */
.time-marker {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: var(--red-marker);
z-index: 5;
pointer-events: none;
}
.time-marker::before {
content: '';
position: absolute;
top: -4px;
left: -5px;
width: 12px;
height: 12px;
background: var(--red-marker);
border-radius: 50%;
}
/* ── TOOLTIP ─────────────────────────────────────────────── */
.tooltip {
position: fixed;
z-index: 100;
background: var(--ink);
color: var(--bone);
border-radius: 8px;
padding: 10px 14px;
font-size: 0.82rem;
font-family: var(--font-body);
pointer-events: none;
opacity: 0;
transition: opacity 0.12s;
max-width: 220px;
line-height: 1.5;
box-shadow: 0 6px 20px rgba(44,26,14,0.3);
}
.tooltip.visible { opacity: 1; }
.tooltip-guest {
font-weight: 700;
font-size: 0.88rem;
margin-bottom: 4px;
}
.tooltip-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
opacity: 0.88;
}
.tooltip-status {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 5px;
}
.tooltip-status.confirmed { background: var(--forest); color: var(--bone); }
.tooltip-status.pending { background: var(--gold); color: var(--ink); }
.tooltip-status.seated { background: var(--terracotta); color: var(--bone); }
.tooltip-status.done { background: var(--warm-gray); color: var(--bone); }
/* ── STATUS DROPDOWN ─────────────────────────────────────── */
.status-dropdown {
position: fixed;
z-index: 50;
background: #fff;
border: 1px solid var(--bone);
border-radius: 10px;
padding: 6px 0;
min-width: 160px;
box-shadow: 0 8px 24px rgba(44,26,14,0.15);
display: none;
flex-direction: column;
}
.status-dropdown.visible { display: flex; }
.status-dropdown-title {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warm-gray);
padding: 4px 14px 6px;
}
.status-option {
display: block;
width: 100%;
text-align: left;
padding: 8px 14px;
border: none;
background: none;
font-family: var(--font-body);
font-size: 0.83rem;
font-weight: 500;
color: var(--ink);
cursor: pointer;
transition: background 0.1s;
}
.status-option:hover { background: var(--cream); }
.cancel-option {
color: var(--terracotta);
font-weight: 600;
}
.status-divider {
border: none;
border-top: 1px solid var(--bone);
margin: 4px 0;
}
/* ── MODAL ───────────────────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 200;
background: rgba(44,26,14,0.45);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s;
}
.modal-overlay.visible {
opacity: 1;
pointer-events: all;
}
.modal {
background: var(--cream);
border-radius: 14px;
width: 100%;
max-width: 420px;
box-shadow: 0 16px 48px rgba(44,26,14,0.25);
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 16px;
border-bottom: 1px solid var(--bone);
background: var(--forest-d);
}
.modal-title {
font-family: var(--font-heading);
font-size: 1.15rem;
font-weight: 800;
color: var(--bone);
}
.modal-close {
background: none;
border: none;
color: var(--warm-gray);
font-size: 1.1rem;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: color 0.12s, background 0.12s;
}
.modal-close:hover { color: var(--bone); background: rgba(255,255,255,0.1); }
.modal-body {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-label {
font-size: 0.78rem;
font-weight: 700;
color: var(--warm-gray);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.form-select,
.form-input {
padding: 9px 12px;
border: 1.5px solid var(--bone);
border-radius: 8px;
background: #fff;
font-family: var(--font-body);
font-size: 0.88rem;
color: var(--ink);
outline: none;
transition: border-color 0.15s;
}
.form-select:focus,
.form-input:focus { border-color: var(--forest); }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px 20px;
border-top: 1px solid var(--bone);
}
.btn-cancel-modal {
padding: 9px 18px;
border: 1.5px solid var(--bone);
border-radius: 8px;
background: #fff;
color: var(--ink);
font-family: var(--font-body);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.12s;
}
.btn-cancel-modal:hover { background: var(--bone); }
.btn-add-modal {
padding: 9px 20px;
border: none;
border-radius: 8px;
background: var(--forest);
color: var(--bone);
font-family: var(--font-body);
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
}
.btn-add-modal:hover { background: var(--forest-d); }
/* ── RESPONSIVE ──────────────────────────────────────────── */
@media (max-width: 700px) {
:root { --col-width: 88px; --label-width: 60px; }
.header { flex-wrap: wrap; height: auto; padding: 12px 16px; }
.legend { display: none; }
.timeline-wrapper { padding: 12px 8px 32px; }
}/* =================================================================
REST-RESERV-TIMELINE — Reservation Timeline Script
Phase 27 Restaurant Theme
================================================================= */
// ── CONSTANTS ────────────────────────────────────────────────────
const TABLES = [
'T1','T2','T3','T4','T5','T6','T7','T8','T9','T10','T11','T12',
'Bar 1','Bar 2','Patio 1','Patio 2'
];
const TIME_START = 18 * 60; // 18:00 in minutes from midnight
const TIME_END = 23 * 60; // 23:00
const SLOT_MIN = 30;
const TOTAL_SLOTS = (TIME_END - TIME_START) / SLOT_MIN; // 10
const CURRENT_TIME = 20 * 60 + 46; // 20:46 — fixed demo "now"
// ── INITIAL RESERVATIONS ─────────────────────────────────────────
let reservations = [
{ id: 1, table: 'T1', guest: 'Martinez', pax: 4, start: 18*60+0, duration: 90, status: 'confirmed', notes: 'Anniversary, window table' },
{ id: 2, table: 'T2', guest: 'Okafor', pax: 2, start: 18*60+30, duration: 90, status: 'pending', notes: '' },
{ id: 3, table: 'T3', guest: 'Chen', pax: 6, start: 19*60+0, duration: 90, status: 'confirmed', notes: 'Birthday — cake at dessert' },
{ id: 4, table: 'T4', guest: 'Hoffman', pax: 3, start: 20*60+0, duration: 90, status: 'seated', notes: 'Allergy: shellfish' },
{ id: 5, table: 'T5', guest: 'Reyes', pax: 2, start: 20*60+30, duration: 90, status: 'confirmed', notes: '' },
{ id: 6, table: 'T6', guest: 'Nguyen', pax: 5, start: 21*60+0, duration: 90, status: 'pending', notes: 'Prefer quiet section' },
{ id: 7, table: 'T7', guest: 'Kowalski', pax: 2, start: 19*60+30, duration: 90, status: 'done', notes: 'Regular guests' },
{ id: 8, table: 'T8', guest: 'Patel', pax: 4, start: 20*60+0, duration: 90, status: 'seated', notes: '' },
{ id: 9, table: 'T9', guest: 'Dubois', pax: 3, start: 18*60+0, duration: 90, status: 'done', notes: '' },
{ id: 10, table: 'T10', guest: 'Svensson', pax: 2, start: 21*60+30, duration: 90, status: 'confirmed', notes: 'VIP — comp dessert' },
{ id: 11, table: 'T11', guest: 'Almeida', pax: 7, start: 19*60+0, duration: 90, status: 'confirmed', notes: 'Corporate booking' },
{ id: 12, table: 'T12', guest: 'Nakamura', pax: 2, start: 20*60+30, duration: 90, status: 'pending', notes: 'First visit' },
{ id: 13, table: 'Bar 1', guest: 'Ellis', pax: 1, start: 19*60+30, duration: 90, status: 'seated', notes: '' },
{ id: 14, table: 'Bar 2', guest: 'Brennan', pax: 2, start: 21*60+0, duration: 90, status: 'confirmed', notes: '' },
{ id: 15, table: 'Patio 1',guest: 'Russo', pax: 4, start: 20*60+0, duration: 90, status: 'confirmed', notes: 'Outdoor preference' },
];
let nextId = 16;
// ── HELPERS ──────────────────────────────────────────────────────
function minToTime(min) {
const h = Math.floor(min / 60);
const m = min % 60;
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
}
function getColWidth() {
// Read from CSS variable
const v = getComputedStyle(document.documentElement).getPropertyValue('--col-width').trim();
return parseInt(v, 10) || 110;
}
// ── RENDER ───────────────────────────────────────────────────────
function render() {
renderTimeHeader();
renderTableLabels();
renderGridBody();
}
function renderTimeHeader() {
const header = document.getElementById('time-header');
header.innerHTML = '';
for (let i = 0; i < TOTAL_SLOTS; i++) {
const min = TIME_START + i * SLOT_MIN;
const cell = document.createElement('div');
cell.className = 'time-cell';
cell.textContent = minToTime(min);
header.appendChild(cell);
}
}
function renderTableLabels() {
const labels = document.getElementById('table-labels');
labels.innerHTML = '';
TABLES.forEach(name => {
const cell = document.createElement('div');
cell.className = 'table-label-cell';
cell.textContent = name;
labels.appendChild(cell);
});
}
function renderGridBody() {
const body = document.getElementById('grid-body');
body.innerHTML = '';
const colW = getColWidth();
TABLES.forEach((table, rowIdx) => {
const row = document.createElement('div');
row.className = 'grid-row';
row.dataset.table = table;
// Background cells
for (let col = 0; col < TOTAL_SLOTS; col++) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
row.appendChild(cell);
}
// Reservation blocks for this table
const tableReservations = reservations.filter(r => r.table === table);
tableReservations.forEach(res => {
const startOffset = res.start - TIME_START; // minutes from grid start
if (startOffset < 0 || startOffset >= (TIME_END - TIME_START)) return;
const leftPct = startOffset / SLOT_MIN;
const widthSlots = res.duration / SLOT_MIN;
const block = document.createElement('div');
block.className = `reservation-block ${res.status}`;
block.style.left = `${leftPct * colW + 2}px`;
block.style.width = `${widthSlots * colW - 4}px`;
block.dataset.id = res.id;
const guestEl = document.createElement('div');
guestEl.className = 'block-guest';
guestEl.textContent = res.guest;
const paxEl = document.createElement('div');
paxEl.className = 'block-pax';
paxEl.textContent = `👥 ${res.pax} · ${minToTime(res.start)}`;
block.appendChild(guestEl);
block.appendChild(paxEl);
// Tooltip events
block.addEventListener('mouseenter', e => showTooltip(e, res));
block.addEventListener('mousemove', e => moveTooltip(e));
block.addEventListener('mouseleave', hideTooltip);
// Click: status dropdown
block.addEventListener('click', e => {
e.stopPropagation();
showStatusDropdown(e, res.id);
});
row.appendChild(block);
});
body.appendChild(row);
});
// Time marker
if (CURRENT_TIME >= TIME_START && CURRENT_TIME <= TIME_END) {
const marker = document.createElement('div');
marker.className = 'time-marker';
const offsetMin = CURRENT_TIME - TIME_START;
const leftPx = (offsetMin / SLOT_MIN) * colW;
marker.style.left = `${leftPx}px`;
body.appendChild(marker);
}
}
// ── TOOLTIP ──────────────────────────────────────────────────────
const tooltip = document.getElementById('tooltip');
function showTooltip(e, res) {
tooltip.innerHTML = `
<div class="tooltip-guest">${res.guest}</div>
<div class="tooltip-row">👥 Party of ${res.pax}</div>
<div class="tooltip-row">🕐 ${minToTime(res.start)} – ${minToTime(res.start + res.duration)}</div>
<div class="tooltip-row">📋 Table ${res.table}</div>
${res.notes ? `<div class="tooltip-row">📝 ${res.notes}</div>` : ''}
<span class="tooltip-status ${res.status}">${capitalise(res.status)}</span>
`;
tooltip.classList.add('visible');
moveTooltip(e);
}
function moveTooltip(e) {
const pad = 16;
let x = e.clientX + pad;
let y = e.clientY + pad;
// Keep within viewport
if (x + 240 > window.innerWidth) x = e.clientX - 240 - pad;
if (y + 160 > window.innerHeight) y = e.clientY - 160 - pad;
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
}
function hideTooltip() {
tooltip.classList.remove('visible');
}
// ── STATUS DROPDOWN ───────────────────────────────────────────────
const statusDropdown = document.getElementById('status-dropdown');
let activeResId = null;
function showStatusDropdown(e, resId) {
activeResId = resId;
const pad = 8;
let x = e.clientX + pad;
let y = e.clientY + pad;
if (x + 170 > window.innerWidth) x = e.clientX - 170 - pad;
if (y + 200 > window.innerHeight) y = e.clientY - 200 - pad;
statusDropdown.style.left = `${x}px`;
statusDropdown.style.top = `${y}px`;
statusDropdown.classList.add('visible');
hideTooltip();
}
function hideStatusDropdown() {
statusDropdown.classList.remove('visible');
activeResId = null;
}
statusDropdown.querySelectorAll('.status-option').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const newStatus = btn.dataset.status;
if (activeResId === null) return;
if (newStatus === 'cancel') {
reservations = reservations.filter(r => r.id !== activeResId);
} else {
const res = reservations.find(r => r.id === activeResId);
if (res) res.status = newStatus;
}
hideStatusDropdown();
render();
});
});
document.addEventListener('click', e => {
if (!statusDropdown.contains(e.target)) hideStatusDropdown();
});
// ── NEW RESERVATION MODAL ─────────────────────────────────────────
const modalOverlay = document.getElementById('modal-overlay');
const btnNew = document.getElementById('btn-new-reservation');
const btnAddModal = document.getElementById('btn-add-modal');
const btnCancelModal = document.getElementById('btn-cancel-modal');
const modalClose = document.getElementById('modal-close');
const formTable = document.getElementById('form-table');
const formTime = document.getElementById('form-time');
const formGuest = document.getElementById('form-guest');
const formPax = document.getElementById('form-pax');
const formNotes = document.getElementById('form-notes');
// Populate selects
TABLES.forEach(t => {
const opt = document.createElement('option');
opt.value = t;
opt.textContent = t;
formTable.appendChild(opt);
});
for (let i = 0; i < TOTAL_SLOTS; i++) {
const min = TIME_START + i * SLOT_MIN;
const opt = document.createElement('option');
opt.value = min;
opt.textContent = minToTime(min);
formTime.appendChild(opt);
}
function openModal() {
formGuest.value = '';
formPax.value = '2';
formNotes.value = '';
modalOverlay.classList.add('visible');
setTimeout(() => formGuest.focus(), 60);
}
function closeModal() {
modalOverlay.classList.remove('visible');
}
btnNew.addEventListener('click', openModal);
btnCancelModal.addEventListener('click', closeModal);
modalClose.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', e => {
if (e.target === modalOverlay) closeModal();
});
btnAddModal.addEventListener('click', () => {
const table = formTable.value;
const start = parseInt(formTime.value, 10);
const guest = formGuest.value.trim();
const pax = Math.max(1, parseInt(formPax.value, 10) || 1);
const notes = formNotes.value.trim();
if (!guest) {
formGuest.focus();
formGuest.style.borderColor = 'var(--terracotta)';
setTimeout(() => { formGuest.style.borderColor = ''; }, 1200);
return;
}
reservations.push({
id: nextId++,
table, guest, pax, start,
duration: 90,
status: 'pending',
notes
});
closeModal();
render();
});
// ── HEADER DATE ───────────────────────────────────────────────────
function setHeaderDate() {
const el = document.getElementById('header-date');
const now = new Date();
el.textContent = now.toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
}
// ── UTILS ─────────────────────────────────────────────────────────
function capitalise(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// ── INIT ──────────────────────────────────────────────────────────
setHeaderDate();
render();
// Re-render on resize to pick up updated col-width CSS variable
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(render, 120);
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reservation Timeline</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Header bar -->
<header class="header">
<div class="header-left">
<h1 class="header-title">Reservation Timeline</h1>
<span class="header-date" id="header-date"></span>
</div>
<div class="header-right">
<div class="legend">
<span class="legend-chip confirmed">Confirmed</span>
<span class="legend-chip pending">Pending</span>
<span class="legend-chip seated">Seated</span>
<span class="legend-chip done">Done</span>
</div>
<button class="btn-new" id="btn-new-reservation">
<span>+</span> New Reservation
</button>
</div>
</header>
<!-- Timeline wrapper -->
<div class="timeline-wrapper">
<div class="timeline-scroll" id="timeline-scroll">
<!-- Corner cell (top-left) -->
<div class="corner-cell">Tables</div>
<!-- Time header row -->
<div class="time-header" id="time-header"></div>
<!-- Table rows -->
<div class="table-labels" id="table-labels"></div>
<div class="grid-body" id="grid-body"></div>
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip" aria-live="polite"></div>
<!-- Status dropdown -->
<div class="status-dropdown" id="status-dropdown">
<div class="status-dropdown-title">Change Status</div>
<button class="status-option" data-status="confirmed">✓ Confirmed</button>
<button class="status-option" data-status="pending">● Pending</button>
<button class="status-option" data-status="seated">◆ Seated</button>
<button class="status-option" data-status="done">■ Done</button>
<hr class="status-divider" />
<button class="status-option cancel-option" data-status="cancel">✕ Cancel Reservation</button>
</div>
<!-- New Reservation Modal -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal">
<div class="modal-header">
<h2 class="modal-title">New Reservation</h2>
<button class="modal-close" id="modal-close">✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="form-table">Table</label>
<select class="form-select" id="form-table"></select>
</div>
<div class="form-group">
<label class="form-label" for="form-time">Start Time</label>
<select class="form-select" id="form-time"></select>
</div>
<div class="form-group">
<label class="form-label" for="form-guest">Guest Name</label>
<input class="form-input" id="form-guest" type="text" placeholder="e.g. Martinez" />
</div>
<div class="form-group">
<label class="form-label" for="form-pax">Party Size</label>
<input class="form-input" id="form-pax" type="number" min="1" max="20" value="2" />
</div>
<div class="form-group">
<label class="form-label" for="form-notes">Notes</label>
<input class="form-input" id="form-notes" type="text" placeholder="e.g. Birthday, window table" />
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel-modal" id="btn-cancel-modal">Cancel</button>
<button class="btn-add-modal" id="btn-add-modal">Add Reservation</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Reservation Timeline
Horizontal Gantt chart where Y = restaurant tables (T1–T12, Bar 1–2, Patio 1–2) and X = service hours (18:00–23:00 in 30-min columns). Each reservation is a colored block spanning its duration (standard: 90 min). Block color = status: confirmed (forest), pending (gold), seated (terracotta), done (warm-gray). Hover tooltip shows guest name, party size, notes. Click a block to open a status-change dropdown. Current time marker (red vertical line) moves across the chart.