Pages Medium
Kitchen Prep Checklist
Pre-service mise en place checklist for kitchen staff: categorized tasks with checkboxes, assignee chips, time estimates, progress ring, and a service-ready sign-off.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* ============================================================
Kitchen Prep Checklist โ Phase 27 Restaurant Theme
============================================================ */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--font-heading: 'Playfair Display', Georgia, serif;
--font-body: 'Inter', system-ui, sans-serif;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-pill: 999px;
--shadow-card: 0 2px 12px rgba(44, 26, 14, 0.08);
--shadow-btn: 0 4px 16px rgba(52, 95, 64, 0.28);
--ring-size: 120px;
--ring-circumference: 314.159; /* 2ฯ ร 50 */
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
body {
background: var(--cream);
color: var(--ink);
font-family: var(--font-body);
min-height: 100vh;
}
/* โโ APP SHELL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.app {
max-width: 680px;
margin: 0 auto;
padding: 0 0 120px;
}
/* โโ HEADER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 20px 20px;
border-bottom: 1px solid var(--bone);
}
.restaurant-name {
font-family: var(--font-heading);
font-size: 1.5rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.01em;
}
.service-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--warm-gray);
margin-top: 2px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.current-time {
font-family: var(--font-heading);
font-size: 1.25rem;
font-weight: 700;
color: var(--forest);
}
/* โโ PROGRESS SECTION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.progress-section {
display: flex;
align-items: center;
gap: 24px;
padding: 24px 20px;
background: white;
border-bottom: 1px solid var(--bone);
}
.progress-ring-wrap {
position: relative;
width: var(--ring-size);
height: var(--ring-size);
flex-shrink: 0;
}
.progress-ring {
transform: rotate(-90deg);
}
.ring-bg {
fill: none;
stroke: var(--bone);
stroke-width: 10;
}
.ring-fill {
fill: none;
stroke: var(--forest);
stroke-width: 10;
stroke-linecap: round;
stroke-dasharray: var(--ring-circumference);
stroke-dashoffset: var(--ring-circumference);
transition: stroke-dashoffset 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.ring-label {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.ring-pct {
font-family: var(--font-heading);
font-size: 1.35rem;
font-weight: 700;
color: var(--forest);
line-height: 1;
}
.ring-sub {
font-size: 0.6rem;
font-weight: 600;
color: var(--warm-gray);
text-align: center;
margin-top: 3px;
line-height: 1.2;
max-width: 64px;
}
.progress-meta {
flex: 1;
min-width: 0;
}
.progress-title {
font-family: var(--font-heading);
font-size: 1.1rem;
font-weight: 700;
color: var(--ink);
margin-bottom: 6px;
}
.progress-hint {
font-size: 0.8rem;
color: var(--warm-gray);
line-height: 1.5;
margin-bottom: 10px;
}
.critical-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
font-weight: 600;
color: var(--ink);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
transition: background 0.3s;
}
.dot--pending {
background: var(--terracotta);
}
.dot--done {
background: var(--forest);
}
/* โโ FILTER ROW โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.filter-row {
display: flex;
gap: 8px;
padding: 16px 20px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
border-bottom: 1px solid var(--bone);
}
.filter-row::-webkit-scrollbar {
display: none;
}
.filter-chip {
flex-shrink: 0;
padding: 7px 16px;
border-radius: var(--radius-pill);
border: 1.5px solid var(--bone);
background: white;
color: var(--warm-gray);
font-family: var(--font-body);
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
transition: all 0.18s ease;
white-space: nowrap;
}
.filter-chip:hover {
border-color: var(--forest);
color: var(--forest);
}
.filter-chip.active {
background: var(--forest);
border-color: var(--forest);
color: var(--bone);
}
/* โโ TASK LIST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.task-list {
padding: 0 20px;
}
.station-group {
margin-bottom: 8px;
}
.station-header {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--terracotta);
padding: 20px 0 8px;
display: flex;
align-items: center;
gap: 10px;
}
.station-header::after {
content: '';
flex: 1;
height: 1px;
background: var(--bone);
}
.task-row {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 0;
border-bottom: 1px solid var(--bone);
cursor: pointer;
transition: opacity 0.18s;
min-height: 56px;
}
.task-row:last-child {
border-bottom: none;
}
.task-row:hover .task-checkbox {
border-color: var(--forest);
}
/* Checkbox */
.task-checkbox {
width: 22px;
height: 22px;
border-radius: 6px;
border: 2px solid var(--bone);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.18s ease;
background: white;
}
.task-checkbox.checked {
background: var(--forest);
border-color: var(--forest);
}
.task-checkbox.checked::after {
content: '';
width: 5px;
height: 9px;
border: 2px solid white;
border-top: none;
border-left: none;
transform: rotate(40deg) translate(-1px, -1px);
display: block;
}
/* Task body */
.task-body {
flex: 1;
min-width: 0;
}
.task-name {
font-size: 0.9rem;
font-weight: 500;
color: var(--ink);
transition: color 0.18s, text-decoration 0.18s;
line-height: 1.4;
}
.task-row.done .task-name {
color: var(--warm-gray);
text-decoration: line-through;
}
.task-tags {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
/* Critical badge */
.badge-critical {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 7px;
border-radius: var(--radius-pill);
background: rgba(196, 98, 45, 0.12);
color: var(--terracotta);
}
.task-row.done .badge-critical {
opacity: 0.45;
}
/* Time estimate */
.task-time {
font-size: 0.7rem;
color: var(--warm-gray);
font-weight: 500;
}
/* Assignee chip */
.assignee-chip {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.02em;
background: rgba(52, 95, 64, 0.1);
color: var(--forest);
transition: opacity 0.18s;
}
.task-row.done .assignee-chip {
opacity: 0.45;
}
/* โโ FOOTER / SIGN-OFF โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 20px 24px;
background: linear-gradient(to top, var(--cream) 70%, transparent);
}
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.footer {
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
}
.signoff-btn {
width: 100%;
height: 56px;
border-radius: var(--radius-lg);
border: none;
background: var(--forest);
color: var(--bone);
font-family: var(--font-body);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: var(--shadow-btn);
transition: background 0.2s, box-shadow 0.2s, transform 0.1s;
max-width: 680px;
margin: 0 auto;
}
.signoff-btn:hover:not(:disabled) {
background: var(--forest-d);
box-shadow: 0 6px 20px rgba(52, 95, 64, 0.35);
}
.signoff-btn:active:not(:disabled) {
transform: scale(0.98);
}
.signoff-btn:disabled {
background: var(--warm-gray);
color: rgba(240, 235, 224, 0.6);
box-shadow: none;
cursor: not-allowed;
}
.signoff-icon {
font-size: 1.15rem;
font-weight: 700;
}
/* Signed-off banner */
.signedoff-banner {
width: 100%;
max-width: 680px;
margin: 0 auto;
height: 56px;
border-radius: var(--radius-lg);
background: var(--forest);
display: flex;
align-items: center;
gap: 14px;
padding: 0 20px;
animation: bannerIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes bannerIn {
from { opacity: 0; transform: translateY(12px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.signedoff-check {
font-size: 1.4rem;
color: var(--gold);
font-weight: 700;
line-height: 1;
}
.signedoff-title {
font-family: var(--font-heading);
font-size: 1rem;
font-weight: 700;
color: var(--bone);
line-height: 1.2;
}
.signedoff-ts {
font-size: 0.72rem;
color: rgba(240, 235, 224, 0.7);
font-weight: 500;
margin-top: 1px;
}
/* โโ HIDDEN UTILITY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
[hidden] {
display: none !important;
}
.station-group[hidden] {
display: none !important;
}/* ============================================================
Kitchen Prep Checklist โ script.js
Phase 27 Restaurant Theme
============================================================ */
'use strict';
// โโ TASK DATA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const TASKS = [
// Garde Manger
{ id: 1, station: 'Garde Manger', name: 'Butcher mise en place', critical: true, assignee: 'Sofia', initials: 'SO', time: '30 min', done: false },
{ id: 2, station: 'Garde Manger', name: 'Salad station setup', critical: false, assignee: 'Diego', initials: 'DG', time: '15 min', done: false },
{ id: 3, station: 'Garde Manger', name: 'Vinaigrettes prepared', critical: false, assignee: 'Diego', initials: 'DG', time: '10 min', done: false },
{ id: 4, station: 'Garde Manger', name: 'Charcuterie board stocked', critical: true, assignee: 'Sofia', initials: 'SO', time: '20 min', done: false },
// Hot Line
{ id: 5, station: 'Hot Line', name: 'Stocks reduced and seasoned', critical: true, assignee: 'Sofia', initials: 'SO', time: '45 min', done: false },
{ id: 6, station: 'Hot Line', name: 'Sauces warmed and held', critical: true, assignee: 'Diego', initials: 'DG', time: '20 min', done: false },
{ id: 7, station: 'Hot Line', name: 'Grill preheated and cleaned', critical: true, assignee: 'Diego', initials: 'DG', time: '10 min', done: false },
{ id: 8, station: 'Hot Line', name: 'Protein portions counted', critical: false, assignee: 'Sofia', initials: 'SO', time: '15 min', done: false },
// Pastry
{ id: 9, station: 'Pastry', name: 'Dessert plates set', critical: false, assignee: 'Ana', initials: 'AN', time: '10 min', done: false },
{ id: 10, station: 'Pastry', name: 'Tarta de queso plated ร8', critical: true, assignee: 'Ana', initials: 'AN', time: '20 min', done: false },
{ id: 11, station: 'Pastry', name: 'Garnishes prepped', critical: false, assignee: 'Ana', initials: 'AN', time: '15 min', done: false },
// Bar
{ id: 12, station: 'Bar', name: 'Ice bins filled', critical: true, assignee: 'Julian', initials: 'JU', time: '10 min', done: false },
{ id: 13, station: 'Bar', name: 'Speed rail stocked', critical: false, assignee: 'Julian', initials: 'JU', time: '15 min', done: false },
{ id: 14, station: 'Bar', name: 'Glassware polished', critical: false, assignee: 'Julian', initials: 'JU', time: '20 min', done: false },
{ id: 15, station: 'Bar', name: 'Cocktail garnishes prepped', critical: false, assignee: 'Julian', initials: 'JU', time: '10 min', done: false },
// FOH
{ id: 16, station: 'FOH', name: 'Tables set and candles lit', critical: true, assignee: 'Camila', initials: 'CA', time: '30 min', done: false },
{ id: 17, station: 'FOH', name: 'Menus distributed', critical: false, assignee: 'Camila', initials: 'CA', time: '10 min', done: false },
{ id: 18, station: 'FOH', name: 'POS stations checked', critical: false, assignee: 'Marco', initials: 'MR', time: '5 min', done: false },
{ id: 19, station: 'FOH', name: 'Reservation list printed', critical: true, assignee: 'Marco', initials: 'MR', time: '5 min', done: false },
{ id: 20, station: 'FOH', name: 'Staff briefing done', critical: true, assignee: 'Marco', initials: 'MR', time: '15 min', done: false },
];
const STATION_ORDER = ['Garde Manger', 'Hot Line', 'Pastry', 'Bar', 'FOH'];
const CIRCUMFERENCE = 314.159; // 2ฯ ร 50
// โโ STATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let activeFilter = 'all';
let signedOff = false;
let serviceStartTime = null;
// โโ DOM REFS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const taskListEl = document.getElementById('taskList');
const ringFill = document.getElementById('ringFill');
const ringPct = document.getElementById('ringPct');
const ringCount = document.getElementById('ringCount');
const critDot = document.getElementById('critDot');
const critText = document.getElementById('critText');
const progressHint = document.getElementById('progressHint');
const signoffBtn = document.getElementById('signoffBtn');
const signedoffBanner = document.getElementById('signedoffBanner');
const signedoffTs = document.getElementById('signedoffTs');
const filterChips = document.querySelectorAll('.filter-chip');
// โโ RENDER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildTaskList() {
taskListEl.innerHTML = '';
STATION_ORDER.forEach(station => {
const tasks = TASKS.filter(t => t.station === station);
const group = document.createElement('div');
group.className = 'station-group';
group.dataset.station = station;
// Station header
const header = document.createElement('div');
header.className = 'station-header';
header.textContent = station;
group.appendChild(header);
tasks.forEach(task => {
const row = document.createElement('div');
row.className = 'task-row' + (task.done ? ' done' : '');
row.dataset.taskId = task.id;
row.setAttribute('role', 'checkbox');
row.setAttribute('aria-checked', task.done ? 'true' : 'false');
row.setAttribute('tabindex', '0');
row.innerHTML = `
<div class="task-checkbox ${task.done ? 'checked' : ''}"></div>
<div class="task-body">
<div class="task-name">${task.name}</div>
<div class="task-tags">
${task.critical ? '<span class="badge-critical">Critical</span>' : ''}
<span class="task-time">โฑ ${task.time}</span>
</div>
</div>
<div class="assignee-chip" title="${task.assignee}">${task.initials}</div>
`;
row.addEventListener('click', () => toggleTask(task.id));
row.addEventListener('keydown', e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleTask(task.id);
}
});
group.appendChild(row);
});
taskListEl.appendChild(group);
});
applyFilter();
}
// โโ TOGGLE TASK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleTask(id) {
if (signedOff) return;
const task = TASKS.find(t => t.id === id);
if (!task) return;
task.done = !task.done;
// Update row DOM
const row = taskListEl.querySelector(`[data-task-id="${id}"]`);
if (row) {
const checkbox = row.querySelector('.task-checkbox');
if (task.done) {
row.classList.add('done');
checkbox.classList.add('checked');
row.setAttribute('aria-checked', 'true');
} else {
row.classList.remove('done');
checkbox.classList.remove('checked');
row.setAttribute('aria-checked', 'false');
}
}
updateProgress();
}
// โโ PROGRESS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateProgress() {
const total = TASKS.length;
const done = TASKS.filter(t => t.done).length;
const critTotal = TASKS.filter(t => t.critical).length;
const critDone = TASKS.filter(t => t.critical && t.done).length;
const pct = Math.round((done / total) * 100);
const allCritDone = critDone === critTotal;
// Ring
const offset = CIRCUMFERENCE - (done / total) * CIRCUMFERENCE;
ringFill.style.strokeDashoffset = offset;
ringPct.textContent = pct + '%';
ringCount.textContent = `${done} of ${total} done`;
// Critical status
critText.textContent = `${critDone} of ${critTotal} critical tasks done`;
if (allCritDone) {
critDot.className = 'dot dot--done';
} else {
critDot.className = 'dot dot--pending';
}
// Hint
if (allCritDone && done < total) {
progressHint.textContent = 'All critical tasks done โ ready to sign off!';
} else if (allCritDone && done === total) {
progressHint.textContent = 'All tasks complete. Kitchen is ready!';
} else {
progressHint.textContent = 'Complete all critical tasks to sign off for service.';
}
// Sign-off button
signoffBtn.disabled = !allCritDone || signedOff;
}
// โโ FILTER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function applyFilter() {
const groups = taskListEl.querySelectorAll('.station-group');
groups.forEach(group => {
const station = group.dataset.station;
if (activeFilter === 'all' || activeFilter === station) {
group.hidden = false;
} else {
group.hidden = true;
}
});
}
filterChips.forEach(chip => {
chip.addEventListener('click', () => {
filterChips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
activeFilter = chip.dataset.station;
applyFilter();
});
});
// โโ SIGN-OFF โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
signoffBtn.addEventListener('click', () => {
if (signoffBtn.disabled || signedOff) return;
signedOff = true;
serviceStartTime = new Date();
const hh = String(serviceStartTime.getHours()).padStart(2, '0');
const mm = String(serviceStartTime.getMinutes()).padStart(2, '0');
const dateStr = serviceStartTime.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
signedoffTs.textContent = `${dateStr} ยท Service opened at ${hh}:${mm}`;
// Swap button for banner
signoffBtn.hidden = true;
signedoffBanner.hidden = false;
});
// โโ CLOCK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateClock() {
const now = new Date();
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
document.getElementById('currentTime').textContent = `${hh}:${mm}`;
}
updateClock();
setInterval(updateClock, 10000);
// โโ INIT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
buildTaskList();
updateProgress();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kitchen Prep Checklist</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>
<div class="app">
<!-- HEADER -->
<header class="header">
<div class="header-left">
<div class="restaurant-name">Casa Marga</div>
<div class="service-label">Pre-Service Prep · Tonight</div>
</div>
<div class="header-right">
<div class="current-time" id="currentTime">--:--</div>
</div>
</header>
<!-- PROGRESS RING -->
<div class="progress-section">
<div class="progress-ring-wrap">
<svg class="progress-ring" width="120" height="120" viewBox="0 0 120 120">
<circle class="ring-bg" cx="60" cy="60" r="50" />
<circle class="ring-fill" id="ringFill" cx="60" cy="60" r="50" />
</svg>
<div class="ring-label">
<span class="ring-pct" id="ringPct">0%</span>
<span class="ring-sub" id="ringCount">0 of 20 done</span>
</div>
</div>
<div class="progress-meta">
<div class="progress-title">Mise en Place Progress</div>
<div class="progress-hint" id="progressHint">Complete all critical tasks to sign off for service.</div>
<div class="critical-status" id="criticalStatus">
<span class="dot dot--pending" id="critDot"></span>
<span id="critText">0 of 9 critical tasks done</span>
</div>
</div>
</div>
<!-- FILTER ROW -->
<div class="filter-row">
<button class="filter-chip active" data-station="all">All</button>
<button class="filter-chip" data-station="Garde Manger">Garde Manger</button>
<button class="filter-chip" data-station="Hot Line">Hot Line</button>
<button class="filter-chip" data-station="Pastry">Pastry</button>
<button class="filter-chip" data-station="Bar">Bar</button>
<button class="filter-chip" data-station="FOH">FOH</button>
</div>
<!-- TASK LIST -->
<main class="task-list" id="taskList">
<!-- populated by script.js -->
</main>
<!-- FOOTER -->
<footer class="footer">
<button class="signoff-btn" id="signoffBtn" disabled>
<span class="signoff-icon">โ</span>
Sign off for service
</button>
<div class="signedoff-banner" id="signedoffBanner" hidden>
<div class="signedoff-check">โ</div>
<div class="signedoff-text">
<div class="signedoff-title">Service Open</div>
<div class="signedoff-ts" id="signedoffTs"></div>
</div>
</div>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>Kitchen Prep Checklist
Pre-service checklist organized by station (Garde Manger / Hot Line / Pastry / Bar / Front of House). Each task has: checkbox, task description, assignee avatar chip, time estimate, and optional โcriticalโ badge. A progress ring at top shows overall % complete. Filter by station or assignee. When all critical tasks are done, the โSign off for serviceโ button becomes active โ tapping it logs the sign-off with timestamp.