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.
This commit is contained in:
tanshu committed 2026-09-22 22:24:53 +05:30
1 parent 7bf67cc299
commit c31b73d553
25 files changed
+1225 -190

No files matched your search

+266 -94
View File
@@ -1,5 +1,7 @@
/* Hops n Grains menu. Steel is the taproom, paper is the menu: the interface
stays quiet so the photographed pages are the only loud thing. */
/* Hops n Grains menus.
The chrome is the shelf; the menus are the paper standing on it. Brass is the
taproom's metal, the logo's teal is the only active colour, and the paper of
the photographed menus is the brightest thing on the page. */
@font-face {
font-family: 'Archivo';
@@ -25,22 +27,23 @@
}
:root {
--steel: #14181b;
--rail: #1f262b;
--paper: #f2efe9;
--steel: #101719;
--rail: #182125;
--paper: #efeae0;
--accent: #0088aa;
--copper: #b2703b;
--ink: #e8e6e1;
--ink-dim: #99a2a8;
--copper: #c08544;
--ink: #e8e5dd;
--ink-dim: #8f979a;
--ink-on-paper: #1b1f22;
--hairline: color-mix(in srgb, var(--copper) 40%, transparent);
--hairline-soft: color-mix(in srgb, var(--copper) 22%, transparent);
--brass-edge: color-mix(in srgb, var(--copper) 42%, transparent);
--hairline: color-mix(in srgb, var(--copper) 26%, transparent);
--hairline-soft: color-mix(in srgb, var(--copper) 14%, transparent);
--sheet-shadow: color-mix(in srgb, #000 45%, transparent);
--step-0: 1rem;
--step-1: 1.25rem;
--step-2: 1.55rem;
--step-3: 2.4rem;
--page-max: 780px;
--content-max: 56rem;
--content-max: 52rem;
}
*,
@@ -60,6 +63,7 @@ body {
font-family: 'Archivo', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: var(--step-0);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
a {
@@ -72,73 +76,118 @@ a {
outline-offset: 3px;
}
/* ---------- Landing ---------- */
/* ---------- Landing: shelves holding menus ---------- */
.landing {
max-width: var(--content-max);
margin: 0 auto;
padding: 0 1.25rem 3rem;
padding: 0 1.1rem 3rem;
}
.masthead {
padding: clamp(2.5rem, 11vh, 5.5rem) 0 1.75rem;
padding: clamp(2.2rem, 10vh, 4.5rem) 0 1.6rem;
}
.wordmark {
margin: 0;
font-size: clamp(1.9rem, 8vw, 2.9rem);
font-weight: 600;
font-stretch: 122%;
letter-spacing: -0.01em;
line-height: 1.05;
font-size: clamp(1.95rem, 8.5vw, 2.9rem);
font-weight: 700;
font-stretch: 118%;
letter-spacing: -0.025em;
line-height: 1;
}
.masthead__tagline {
margin: 0.4rem 0 0;
margin: 0.5rem 0 0;
color: var(--ink-dim);
font-size: var(--step-1);
font-size: 0.98rem;
}
.rows {
border-top: 1px solid var(--hairline);
}
.row {
.shelves {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding: 1.1rem 0.15rem;
border-bottom: 1px solid var(--hairline);
flex-direction: column;
margin-right: -1.1rem;
}
.row__label {
font-size: clamp(1.45rem, 6.5vw, 2rem);
font-weight: 600;
font-stretch: 116%;
line-height: 1.15;
.shelf {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 0.3rem;
min-height: 6.6rem;
padding: 1.2rem 6.9rem 1.25rem 0.1rem;
overflow: hidden;
border-top: 1px solid var(--brass-edge);
}
.row__meta {
color: var(--ink-dim);
font-size: 0.95rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
.shelf:last-child {
border-bottom: 1px solid var(--brass-edge);
}
.row:hover .row__label,
.row:focus-visible .row__label {
/* the shadow the shelf above casts onto what it holds */
.shelf::after {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1.3rem;
background: linear-gradient(var(--sheet-shadow), transparent);
pointer-events: none;
}
.shelf__peek {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 4.9rem;
box-shadow: -1px 0 0 var(--sheet-shadow);
}
.shelf__peek img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
transform: translateY(4%) scale(1.02);
transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.shelf:hover .shelf__peek img,
.shelf:focus-visible .shelf__peek img {
transform: translateY(0) scale(1.07);
}
.shelf__name {
font-size: clamp(1.5rem, 6.4vw, 2rem);
font-weight: 700;
font-stretch: 112%;
letter-spacing: -0.02em;
line-height: 1.02;
}
.shelf:hover .shelf__name,
.shelf:focus-visible .shelf__name {
color: var(--accent);
}
.shelf__pages {
color: var(--ink-dim);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
}
.empty {
max-width: 34ch;
margin: 0;
margin: 1rem 0 0;
color: var(--ink-dim);
font-size: var(--step-1);
}
/* ---------- Section page: the rail and the pour line ---------- */
/* ---------- Section page: the bar and the switcher ---------- */
.bar {
position: sticky;
@@ -146,42 +195,50 @@ a {
z-index: 20;
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.5rem 0.9rem 0.4rem;
background: var(--rail);
gap: 0.7rem;
padding: 0.5rem 0.85rem 0.45rem;
background: color-mix(in srgb, var(--rail) 94%, transparent);
backdrop-filter: blur(6px);
}
.bar__brand {
font-size: 0.95rem;
font-weight: 600;
font-stretch: 118%;
white-space: nowrap;
}
.rail {
display: flex;
flex: 1 1 auto;
gap: 0.15rem;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
align-items: center;
}
.rail::-webkit-scrollbar {
display: none;
.bar__brand img {
display: block;
width: 24px;
height: 24px;
}
.rail__item {
padding: 0.35rem 0.6rem;
color: var(--ink-dim);
font-size: 0.95rem;
white-space: nowrap;
}
.rail__item.is-current {
background: var(--accent);
color: var(--steel);
.switcher {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.5rem;
border: 1px solid transparent;
color: var(--ink);
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.switcher:hover {
border-color: var(--brass-edge);
}
.switcher__caret {
width: 0.45rem;
height: 0.45rem;
border-right: 1.5px solid var(--copper);
border-bottom: 1.5px solid var(--copper);
transform: rotate(45deg) translate(-0.1rem, -0.1rem);
transition: transform 0.2s ease;
}
.switcher[aria-expanded='true'] .switcher__caret {
transform: rotate(-135deg) translate(-0.05rem, -0.05rem);
}
.pour {
@@ -200,25 +257,130 @@ a {
background: var(--copper);
}
/* ---------- The switcher sheet ---------- */
.sheet {
position: fixed;
inset: 0;
z-index: 40;
display: flex;
align-items: flex-end;
background: color-mix(in srgb, #05090a 74%, transparent);
}
.sheet[hidden] {
display: none;
}
.sheet__inner {
width: 100%;
max-height: 88vh;
overflow-y: auto;
padding: 1.1rem 1rem calc(1.2rem + env(safe-area-inset-bottom));
background: var(--rail);
border-top: 2px solid var(--copper);
animation: sheet-rise 0.24s cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes sheet-rise {
from {
transform: translateY(1.6rem);
opacity: 0.5;
}
to {
transform: none;
opacity: 1;
}
}
.sheet__title {
margin: 0 0 0.5rem;
color: var(--ink-dim);
font-size: 0.85rem;
}
.sheet__list {
margin: 0;
padding: 0;
list-style: none;
}
.sheet__item {
display: grid;
grid-template-columns: 2.4rem 1fr auto;
align-items: center;
gap: 0.85rem;
padding: 0.5rem 0.4rem;
border-bottom: 1px solid var(--hairline-soft);
}
.sheet__item.is-current {
background: color-mix(in srgb, var(--accent) 13%, transparent);
}
.sheet__peek {
width: 2.4rem;
height: 3rem;
overflow: hidden;
background: var(--paper);
}
.sheet__peek img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
}
.sheet__name {
font-size: 1.12rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.sheet__state {
color: var(--ink-dim);
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
.sheet__item.is-current .sheet__state {
color: var(--accent);
}
.sheet__close {
margin-top: 0.9rem;
padding: 0.5rem 0.85rem;
border: 1px solid var(--brass-edge);
background: none;
color: var(--ink);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
/* ---------- The pages themselves ---------- */
.section-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
max-width: var(--page-max);
margin: 1.35rem auto 0.85rem;
padding: 0 1rem;
margin: 1.2rem auto 0.75rem;
padding: 0 0.9rem;
}
.section-heading__label {
font-size: var(--step-2);
font-weight: 600;
font-stretch: 116%;
font-weight: 700;
letter-spacing: -0.02em;
}
.section-heading__date {
color: var(--ink-dim);
font-size: 0.9rem;
font-size: 0.88rem;
font-variant-numeric: tabular-nums;
}
@@ -239,20 +401,19 @@ a {
.notice--admin {
max-width: none;
border-left-color: var(--accent);
background: color-mix(in srgb, var(--accent) 22%, var(--rail));
background: color-mix(in srgb, var(--accent) 20%, var(--rail));
}
/* ---------- The Pages ---------- */
.pages {
max-width: var(--page-max);
margin: 0 auto;
padding: 0 0.75rem 1.25rem;
padding: 0 0.7rem 1.25rem;
}
.page {
margin: 0 0 0.7rem;
margin: 0 0 0.6rem;
background: var(--paper);
box-shadow: 0 1px 0 color-mix(in srgb, #000 35%, transparent);
}
.page__zoom {
@@ -270,7 +431,7 @@ a {
.foot {
max-width: var(--page-max);
margin: 0 auto 4.5rem;
padding: 0 1rem 1rem;
padding: 0 0.9rem 1rem;
border-top: 1px solid var(--hairline-soft);
}
@@ -281,7 +442,7 @@ a {
.fold summary {
color: var(--ink-dim);
font-size: 0.95rem;
font-size: 0.92rem;
cursor: pointer;
}
@@ -332,7 +493,7 @@ a {
.older__date {
color: var(--ink-dim);
font-size: 0.9rem;
font-size: 0.88rem;
font-variant-numeric: tabular-nums;
}
@@ -349,18 +510,18 @@ a {
.counter {
position: fixed;
right: 0.9rem;
bottom: calc(0.9rem + env(safe-area-inset-bottom));
right: 0.85rem;
bottom: calc(0.85rem + env(safe-area-inset-bottom));
z-index: 30;
display: inline-flex;
align-items: baseline;
gap: 0.3rem;
padding: 0.45rem 0.7rem;
border: 1px solid var(--hairline);
border: 1px solid var(--brass-edge);
background: var(--paper);
color: var(--ink-on-paper);
font: inherit;
font-size: 0.9rem;
font-size: 0.88rem;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
@@ -376,9 +537,9 @@ a {
.zoom {
position: fixed;
inset: 0;
z-index: 40;
z-index: 50;
overflow: auto;
background: rgba(8, 10, 12, 0.95);
background: rgba(6, 9, 10, 0.96);
-webkit-overflow-scrolling: touch;
}
@@ -395,7 +556,7 @@ a {
right: 0.6rem;
z-index: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--hairline);
border: 1px solid var(--brass-edge);
background: var(--paper);
color: var(--ink-on-paper);
font: inherit;
@@ -404,7 +565,17 @@ a {
/* ---------- Wider screens ---------- */
@media (min-width: 60rem) {
@media (min-width: 62rem) {
.shelves {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 2.5rem;
}
.shelf {
padding-right: 5.6rem;
}
.pages {
max-width: 820px;
}
@@ -412,6 +583,7 @@ a {
@media print {
.bar,
.sheet,
.counter,
.fold,
.notice {