Categories were baked into config/<site>.site.json, so adding one meant a rebuild and a redeploy. They are now data: data/sections.json, seeded from the config the first time, and managed in the admin (add, rename, reorder, remove). A category's web address is fixed when it is created because that is what a QR code points at, and a category cannot be removed while it still holds menus. The landing and the top bar were the ugliest parts. The landing was a column of hairlines with tiny meta — a broadsheet, and the paper that makes this app worth looking at was nowhere on it. It is now a shelf: each category is a row whose right edge is a sliver of that menu's first page, bleeding off the screen, with the shelf edge in brass and the name in heavy tight Archivo. The bar was a row of teal pills that would not survive more categories: it now names the menu you are reading and opens a sheet listing the rest, each with its own paper sliver, and the current one is the only teal thing on screen. The sheet is a link to the landing page, so navigating still works without the script. Also fixes the category manager's own layout, which I shipped unstyled, and the markup contract the route tests described.
169 lines
5.4 KiB
JavaScript
169 lines
5.4 KiB
JavaScript
/* Progressive enhancement only: the pour line, the page counter, and
|
|
tap-to-zoom. Without this file every menu is still readable and every
|
|
Page opens as a plain link. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var reducesMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
var pages = document.querySelector('[data-pages]');
|
|
var fill = document.querySelector('[data-pour]');
|
|
var counter = document.querySelector('[data-counter]');
|
|
var counterCurrent = counter && counter.querySelector('[data-counter-current]');
|
|
var figures = pages ? Array.prototype.slice.call(pages.querySelectorAll('[data-page]')) : [];
|
|
var currentPage = 1;
|
|
|
|
function updatePour() {
|
|
if (!pages || !fill) return;
|
|
var rect = pages.getBoundingClientRect();
|
|
var travel = rect.height - window.innerHeight;
|
|
if (travel <= 0) {
|
|
fill.style.width = '100%';
|
|
return;
|
|
}
|
|
var progress = Math.min(Math.max(-rect.top, 0), travel) / travel;
|
|
fill.style.width = (progress * 100).toFixed(1) + '%';
|
|
}
|
|
|
|
function updateCounter() {
|
|
if (!counter || figures.length === 0) return;
|
|
var middle = window.innerHeight / 2;
|
|
var best = 1;
|
|
var bestDistance = Infinity;
|
|
figures.forEach(function (figure, index) {
|
|
var rect = figure.getBoundingClientRect();
|
|
var distance = Math.abs(rect.top + rect.height / 2 - middle);
|
|
if (distance < bestDistance) {
|
|
bestDistance = distance;
|
|
best = index + 1;
|
|
}
|
|
});
|
|
if (best !== currentPage) {
|
|
currentPage = best;
|
|
if (counterCurrent) counterCurrent.textContent = String(best);
|
|
}
|
|
}
|
|
|
|
function onScroll() {
|
|
updatePour();
|
|
updateCounter();
|
|
}
|
|
|
|
if (counter) counter.hidden = false;
|
|
if (figures.length > 0) {
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
window.addEventListener('resize', onScroll);
|
|
onScroll();
|
|
}
|
|
|
|
if (counter) {
|
|
counter.addEventListener('click', function () {
|
|
var fold = document.querySelector('.fold--index');
|
|
if (!fold) return;
|
|
fold.open = true;
|
|
fold.scrollIntoView({ behavior: reducesMotion ? 'auto' : 'smooth', block: 'center' });
|
|
var first = fold.querySelector('a');
|
|
if (first) first.focus({ preventScroll: true });
|
|
});
|
|
}
|
|
|
|
var overlay = null;
|
|
|
|
function closeOverlay() {
|
|
if (!overlay) return;
|
|
overlay.remove();
|
|
overlay = null;
|
|
document.body.style.overflow = '';
|
|
document.removeEventListener('keydown', onKeydown);
|
|
}
|
|
|
|
function onKeydown(event) {
|
|
if (event.key === 'Escape') closeOverlay();
|
|
}
|
|
|
|
function openOverlay(href, alt) {
|
|
overlay = document.createElement('div');
|
|
overlay.className = 'zoom';
|
|
overlay.setAttribute('role', 'dialog');
|
|
overlay.setAttribute('aria-modal', 'true');
|
|
overlay.setAttribute('aria-label', alt);
|
|
|
|
var close = document.createElement('button');
|
|
close.type = 'button';
|
|
close.className = 'zoom__close';
|
|
close.textContent = 'Close';
|
|
|
|
var image = document.createElement('img');
|
|
image.src = href;
|
|
image.alt = alt;
|
|
|
|
overlay.appendChild(close);
|
|
overlay.appendChild(image);
|
|
overlay.addEventListener('click', function (event) {
|
|
if (event.target === overlay || event.target === close) closeOverlay();
|
|
});
|
|
document.addEventListener('keydown', onKeydown);
|
|
document.body.appendChild(overlay);
|
|
document.body.style.overflow = 'hidden';
|
|
close.focus();
|
|
}
|
|
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-zoom]'), function (link) {
|
|
link.addEventListener('click', function (event) {
|
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
|
|
return;
|
|
}
|
|
var image = link.querySelector('img');
|
|
event.preventDefault();
|
|
openOverlay(link.getAttribute('href'), (image && image.alt) || 'Menu page');
|
|
});
|
|
});
|
|
|
|
/* ---------- The menu switcher ----------
|
|
The bar names the menu you are reading; tapping it lists the rest. The
|
|
switcher is a link to the landing page, so it still works without this
|
|
file — the script just opens the sheet instead of navigating. */
|
|
var switcher = document.querySelector('[data-switcher]');
|
|
var sheet = document.querySelector('[data-sheet]');
|
|
var sheetClose = document.querySelector('[data-sheet-close]');
|
|
|
|
function closeSheet() {
|
|
if (!sheet) return;
|
|
sheet.hidden = true;
|
|
if (switcher) {
|
|
switcher.setAttribute('aria-expanded', 'false');
|
|
switcher.focus({ preventScroll: true });
|
|
}
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
function openSheet() {
|
|
if (!sheet) return;
|
|
sheet.hidden = false;
|
|
if (switcher) switcher.setAttribute('aria-expanded', 'true');
|
|
document.body.style.overflow = 'hidden';
|
|
var target = sheet.querySelector('.sheet__item') || sheetClose;
|
|
if (target) target.focus({ preventScroll: true });
|
|
}
|
|
|
|
if (switcher && sheet) {
|
|
switcher.addEventListener('click', function (event) {
|
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
if (sheet.hidden) openSheet();
|
|
else closeSheet();
|
|
});
|
|
|
|
sheet.addEventListener('click', function (event) {
|
|
if (event.target === sheet) closeSheet();
|
|
});
|
|
|
|
if (sheetClose) sheetClose.addEventListener('click', closeSheet);
|
|
|
|
document.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Escape' && !sheet.hidden) closeSheet();
|
|
});
|
|
}
|
|
})();
|