Files
menu/public/app.js
T
tanshu e64f65642f Add ESLint and Prettier
Prettier owns formatting: .prettierrc.json, format and format:check scripts, and
a one-off pass over the repo so the check is green from the start. Embedded
language formatting is off deliberately - the renderers build HTML in html
tagged templates and letting Prettier reflow those would change the markup
customers receive. docs/ is excluded because it is hand-wrapped prose, and shell
scripts have no Prettier parser.

ESLint covers the JavaScript: the two browser scripts, the build and check
scripts, and its own config. no-var is scoped out for the browser scripts on
purpose; they are plain ES5-shaped IIFEs and a hoisting difference would only
show up in a browser.

ESLint cannot lint the TypeScript here: typescript@7 is the native port with no
JavaScript compiler API, and typescript-eslint requires a 4.8-6.1 peer, so it
refuses to install and forcing it through would leave a parser that cannot read
the compiler. tsc --noEmit stays the TypeScript gate; README records the two
ways to change that.

lint and format:check now run in deploy.sh, and VS Code formats on save with
Prettier and surfaces ESLint findings.
2026-09-22 23:15:08 +05:30

121 lines
3.8 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');
});
});
})();