Archived
The ported markup was written a page at a time, so the same thing was said several ways. Counted before touching anything: nine variants of the page gutter, seven max-widths, six link styles, two definitions of an input, and one stray shadow. Now: `container` (which already centres and pads), `measure`/`measure-wide`, `link`/`link-plain`/ `link-on-dark`, one `field`, `photo`, `page-head`. Nothing in a template writes `bg-white rounded-… shadow-…` or `underline underline-offset-4` any more — 119 uses of `panel`, 82 of `label`, 52 of `panel-lift`, 17 of `link`. TWO REAL FIXES FELL OUT OF IT, not just tidying: - Every section said `container mx-auto px-4`: `mx-auto` twice, and a px-4 that beat the responsive padding inside `container`. The gutter was 16px at every width, including on a 27-inch screen. Removing the copy-paste restores sm:px-6 lg:px-8, which is most of why the pages now breathe. - The public form and the admin had grown two different inputs — cream vs white, ring-2 vs ring-1, py-2.5 vs py-2, and different text sizes. There is one `field` now, and the same focus ring as everything else. The admin uses the same names where they are the same thing (`link`, `field`, `h4`, `--radius-*`) and keeps its own denser buttons, because it is a tool and not a shop front. 53 tests green. Every class used across all nine pages resolves in the compiled stylesheet — that check is what makes a sweep this size safe to do with a script. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
98 lines
4.5 KiB
HTML
98 lines
4.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/page :: page(${title}, ${description}, ${path}, ~{::content})}">
|
|
<body>
|
|
<div th:fragment="content" class="min-h-screen bg-bakery-50">
|
|
|
|
<header class="page-head">
|
|
<h1 class="h1 text-bakery-900">Our products</h1>
|
|
<p class="mt-4 text-bakery-800 measure leading-relaxed">
|
|
What comes out of the kitchen on Main Street. Cakes and decorated cookies are made to order, so
|
|
most of what follows started as somebody describing what they wanted.
|
|
</p>
|
|
</header>
|
|
|
|
<div class="container pb-16">
|
|
<!--/*
|
|
The category filter: links, not buttons. Each filtered view is now a URL you can bookmark or send
|
|
to somebody, the browser's back button does what it looks like it does, and a crawler can see all
|
|
forty items instead of whichever twelve the default filter showed. The filtering itself has always
|
|
been the server's job — this just stopped pretending otherwise.
|
|
*/-->
|
|
<div class="flex flex-wrap justify-center gap-4 mb-12">
|
|
<a th:each="category : ${categories}"
|
|
th:href="${category == 'All' ? '/products' : '/products?category=' + #uris.escapeQueryParam(category)}"
|
|
th:text="${category}"
|
|
th:aria-current="${category == selected ? 'page' : null}"
|
|
th:class="${category == selected} ? 'chip-on' : 'chip'">All</a>
|
|
</div>
|
|
|
|
<!--/* Deliberately unanimated, as before: filtering used to run a layout reflow plus an enter/exit
|
|
fade on every card, which on a 40-card grid reads as the page lurching rather than
|
|
responding. The only motion left is the shadow on hover. */-->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
<div th:each="product : ${products}"
|
|
class="group panel-lift overflow-hidden">
|
|
<div class="relative w-full overflow-hidden">
|
|
<div th:replace="~{:: gallery(${product})}"></div>
|
|
</div>
|
|
<!--/* The caption sits on the cream, so the photo reads as mounted on the card rather than as
|
|
the card itself. */-->
|
|
<div class="panel-head border-b-0 border-t px-6 py-5 text-center">
|
|
<h3 class="h4 text-bakery-900" th:text="${product.name}">Name</h3>
|
|
<span class="label text-bakery-600" th:text="${product.category}">Category</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p th:if="${#lists.isEmpty(products)}" class="text-center text-bakery-800">
|
|
Nothing in that category just now.
|
|
<a href="/products" class="link-plain">See everything</a>.
|
|
</p>
|
|
</div>
|
|
|
|
<!--/* A catalogue with no way to order from it is a gallery. */-->
|
|
<section class="section bg-bakery-800 text-white">
|
|
<div class="container">
|
|
<div class="measure text-center">
|
|
<h2 class="h3">Want one of these, or something else?</h2>
|
|
<p class="mt-4 text-bakery-100 leading-relaxed">
|
|
Cakes and decorated cookies are made to order. Tell us the date and roughly what you have in
|
|
mind — a tractor, a retirement, the class of 1964 — and we will work from there.
|
|
</p>
|
|
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
|
|
<a href="/contact"
|
|
class="pill-cream">
|
|
Ask about an order
|
|
</a>
|
|
<a href="/catering"
|
|
class="pill-ghost">
|
|
Ordering for a crowd
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!--/*
|
|
The photo on a card.
|
|
|
|
A scroll-snap strip rather than a stack of absolutely positioned images cross-fading: it is one square
|
|
photo at a time either way, but this version swipes on a phone and scrolls with a trackpad with no
|
|
script at all. /js/gallery.js adds the arrows and the dots on top. They are created there rather than
|
|
rendered here on purpose — a control that does nothing without JavaScript is worse than no control.
|
|
*/-->
|
|
<div th:fragment="gallery(product)" class="relative aspect-square bg-bakery-100">
|
|
<div class="gallery flex h-full w-full overflow-x-auto snap-x snap-mandatory no-scrollbar"
|
|
th:attr="aria-label=${#lists.size(product.images) > 1 ? product.name + ' — ' + #lists.size(product.images) + ' photos' : null}"
|
|
th:aria-roledescription="${#lists.size(product.images) > 1 ? 'carousel' : null}">
|
|
<img th:each="image, i : ${product.images}" th:src="${image}"
|
|
th:alt="${i.index == 0 ? product.name : ''}"
|
|
th:aria-hidden="${i.index == 0 ? null : 'true'}"
|
|
loading="lazy" decoding="async"
|
|
class="snap-center shrink-0 h-full w-full object-cover">
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|