Commit Graph
10 Commits
Author SHA1 Message Date
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
tanshu 32eeed144f Let the owner change the name, the icon and the dates
Three things that needed a rebuild and a redeploy, or had no way in at all, now
have a Site identity panel in the admin: the restaurant's name and tagline, an
SVG icon upload, and a switch for whether a menu says when it was published.
They live in data/site.json and data/brand/icon.svg, extending ADR 0007: what
the owner changes is content, and the config is the seed plus the theme, which
still needs a rebuild.

An uploaded SVG is a document that would run with this origin's authority, so it
is reduced to shapes on the way in and served with default-src 'none'. The
archive list keeps its dates even when the switch is off, because those dates
are how one old menu is told from another.

Also fixes the live check's own trap: the app caches identity in memory, so the
tests drive it through the app rather than a second store instance, which would
be a second writer.
2026-09-22 23:11:40 +05:30
tanshu b13042f0db Adopt the Stitch design system
Google Stitch produced an Atelier Dining & Hospitality system for this app: warm
ivory linen and espresso for the guest surfaces, burnished gold for the single
active thing, Playfair Display for the restaurant's voice and Plus Jakarta Sans
for everything transactional, editorial rows with dot leaders, pill controls,
hairline containment instead of drop shadows. The four screens and the system
document are kept in docs/design/stitch/ as the source.

Both fonts are now self-hosted with their licences, the guest stylesheet is
rebuilt on the system, and the admin is the same system in its dark mode
(espresso workspace, obsidian pods, gold actions) by overriding the same tokens
at a higher specificity, since the per-Site theme is injected inline after the
stylesheets. The Site theme in config/ moves to the system's palette, which is
where per-Site branding belongs; the old Archivo font is dropped.
2026-09-22 23:07:38 +05:30
tanshu a29c5c4b0c Make the navigation out of the menus themselves
Variant B of the prototype (/prototype/menu-design branch): each category is a
card whose face is the first page of that menu, so the landing is built from the
restaurant's own paper instead of labels on a dark page. The row scrolls and
snaps on a phone and wraps into a grid on a desktop; the cards are drawn from
the existing tokens, not the prototype's hexes. The bar's links become filled
segments, the current one the only teal thing on screen.

Fixes the scroller's first card being clipped: a negative-margin full bleed was
fighting scroll-snap, so the row now insets its first card and snaps to the same
inset.
2026-09-22 22:39:01 +05:30
tanshu c5c0b7d005 Put the other menus in the top bar
The bar hid them behind a switcher and a sheet. It now names every menu that
exists, which is what was asked for; it scrolls sideways when the categories
outgrow the width, and the current one is the only filled item. Without the
script it is simply a row of links, so nothing is lost when JavaScript is off.
2026-09-22 22:35:38 +05:30
tanshu c31b73d553 Let the owner manage categories, and give the menus a shelf
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.
2026-09-22 22:24:53 +05:30
tanshu 7bf67cc299 Make reordering work on the page you are actually looking at
Two defects, both found by driving the real editor in Firefox:

1. The editor drew grab handles with a grab cursor on sets it refuses to
   reorder, so the live Food menu looked draggable and did nothing. Handles and
   the move buttons are now rendered only when reordering is possible, and the
   notice says what can still be done.

2. Reordering was saved only from dragend. Browsers suppress mouse events during
   a drag and dragend is not guaranteed to arrive, which left the rows visibly
   reordered while the server kept the old order — the drag looked like it
   worked and then reverted on reload. The order now commits from dragover after
   a pause in the drag, with drop/dragend/mouse events as faster paths, deduped
   against the last confirmed order and reported in the page.

To make (1) coherent the Current set may now be reordered while its Pages stay
fixed: order is presentation, media URLs are content-hashed and pages are
rendered per request, so nothing stale can be served (ADR 0006). Archives stay
frozen. Admin pages are no-store and assets cache for 5 minutes, so a fix like
this one is not masked by a stale admin.js.

scripts/check-dnd.mjs drives the real editor and asserts the drag persists; it
is the only automated check that reaches this path.
2026-09-22 22:01:52 +05:30
tanshu a635c5a214 Answer an oversized photo with the reason, not a 500
A file past the size limit makes the multipart stream itself throw, which
escaped the per-file catch and reached the error handler: the phone that just
sent a 35 MB photo got 'Something went wrong'. Catch the limit errors around the
part loop, say which limit was hit, and report it once rather than twice. Also
asserts that derivatives carry no EXIF while the Original keeps the uploaded
bytes.
2026-09-22 21:41:12 +05:30
tanshu 58ecfa0dc3 Publish menus from the browser instead of scp
The admin signs in with the shared password, creates a Menu Set, drops photos
into it (shrunk in the browser first), reorders Pages by dragging or with
up/down buttons, previews the Draft exactly as customers would see it, and
publishes it over the previous set. Restore and delete close the loop. Mutations
need a session, a same-origin request and a CSRF token derived from the session;
a Draft's media is served only to a session holder.
2026-09-22 21:22:26 +05:30
tanshu 9cb4021229 Serve the customer-facing menus
Landing page, Section pages and archived sets rendered per request from the
store, with the approved design: hand-written CSS, self-hosted Archivo, a
copper pour line in the Section rail, an admin-only Thumbnail and a text page
index. Media is keyed by Page id so URLs survive publishing, and a Draft's
media needs a session. Includes the scrypt admin auth and the login throttle
the media rule needs.
2026-09-22 21:18:49 +05:30