seating.papp
All 60 lines. No libraries, nothing hidden. Read it before you run it.
<!doctype html>
<meta charset="utf-8">
<meta name="papp-write" content="./seating.csv">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 4 64 64'><circle cx='28' cy='36' r='11' fill='none' stroke='%23111114' stroke-width='4'/><g fill='%23111114'><circle cx='28' cy='15' r='4.5'/><circle cx='42.85' cy='21.15' r='4.5'/><circle cx='49' cy='36' r='4.5'/><circle cx='28' cy='57' r='4.5'/><circle cx='13.15' cy='50.85' r='4.5'/><circle cx='7' cy='36' r='4.5'/></g><g fill='%23ff4f00'><circle cx='13.15' cy='21.15' r='4.5'/><circle cx='42.85' cy='50.85' r='4.5'/></g></svg>">
<title>Seating</title>
<style>
:root { color-scheme: light dark; --paper: light-dark(#fff, #0f0f11); --well: light-dark(#f4f4f5, #1c1c20); --ink: light-dark(#0f0f11, #f2f2f3); --soft: light-dark(#6b6b72, #9b9ba4); --rule: light-dark(#e4e4e7, #2b2b31); --accent: #ff4f00; }
body { display: grid; grid-template-columns: 15rem 1fr; height: 100vh; margin: 0; background: var(--paper); color: var(--ink); font: 13px/1.4 system-ui, sans-serif; }
#pile { overflow: auto; padding: 20px 16px; border-right: 1px solid var(--rule); background: var(--well); }
#pile::before { content: 'Not seated yet'; display: block; margin-bottom: 12px; color: var(--soft); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
#tables { display: flex; flex-wrap: wrap; align-content: start; gap: 8px 24px; padding: 20px; overflow: auto; }
.table { position: relative; width: 21rem; height: 21rem; }
.table::before { content: attr(data-n); position: absolute; inset: 6.4rem; display: grid; place-items: center; border-radius: 50%; background: var(--well); color: var(--soft); font-size: 1.6rem; font-weight: 600; }
.table .guest { position: absolute; top: 50%; left: 50%; margin: 0; translate: -50% -50%; transform: rotate(calc(var(--seat) * 36deg)) translateY(-8.4rem) rotate(calc(var(--seat) * -36deg)); }
.guest { display: inline-block; margin: 3px; padding: 3px 9px; font-size: 12px; border: 1px solid var(--rule); border-radius: 99px; background: var(--paper); white-space: nowrap; cursor: grab; transition: border-color 0.15s; }
.guest:hover { border-color: var(--ink); }
.apart { border-color: var(--accent); background: var(--accent); color: #0f0f11; font-weight: 600; }
</style>
<div id="pile"></div>
<div id="tables"></div>
<script type="module">
// guests.csv: a header row, then "name,apart" per guest, where apart names someone they must not sit with.
// seating.csv: "guest,table" for the venue, saved on every drop. Tables seat 10.
const guests = (await papp.readFile('guests.csv')).trim().split('\n').slice(1).map(line => line.trim().split(','));
const seating = (await papp.exists('seating.csv')) ? (await papp.readFile('seating.csv')).trim().split('\n').slice(1) : [];
const seats = Object.fromEntries(seating.map(line => line.trim().split(',')));
for (let n = 1; n <= Math.ceil(guests.length / 10); n++) {
const table = document.createElement('div');
table.className = 'table';
table.dataset.n = n;
tables.append(table);
}
addEventListener('dragover', event => event.target.closest('.table, #pile') && event.preventDefault());
addEventListener('drop', async event => {
const place = event.target.closest('.table, #pile');
const name = event.dataTransfer.getData('text/plain');
if (place === pile) delete seats[name];
else if (place.children.length < 10) seats[name] = place.dataset.n;
await papp.writeFile('seating.csv', ['guest,table', ...Object.entries(seats).map(seat => seat.join(','))].join('\n') + '\n');
draw();
});
draw();
function draw() {
for (const place of [pile, ...tables.children]) place.replaceChildren();
for (const [name, apart] of guests) {
const table = seats[name];
const place = table ? tables.children[table - 1] : pile;
const guest = Object.assign(document.createElement('span'), { className: 'guest', textContent: name, draggable: true });
guest.classList.toggle('apart', Boolean(table) && guests.some(([other, otherApart]) => seats[other] === table && (other === apart || otherApart === name)));
guest.style.setProperty('--seat', place.children.length);
guest.ondragstart = event => event.dataTransfer.setData('text/plain', name);
place.append(guest);
}
document.title = `Seating · ${Object.keys(seats).length} of ${guests.length} seated`;
}
</script>