Archived
Catering, and a pure Java/Spring site: Thymeleaf front to back #10
@@ -67,19 +67,24 @@
|
||||
: (${#lists.size(table.tiers) == 2} ? 'sm:grid-cols-2 max-w-3xl mx-auto'
|
||||
: 'sm:grid-cols-2 lg:grid-cols-3')">
|
||||
<article th:each="tier, t : ${table.tiers}"
|
||||
class="flex flex-col panel-lift p-6 md:p-8">
|
||||
<p class="label text-bakery-600" th:text="${tier.label}">Small</p>
|
||||
<p th:if="${tier.price}" class="price mt-2 text-bakery-900" th:text="${tier.price}">$24</p>
|
||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
||||
<p th:unless="${tier.price}" class="price mt-2 text-2xl text-bakery-700">Ask us</p>
|
||||
<!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a
|
||||
head count leaves this blank rather than saying it twice. */-->
|
||||
<p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 6–8 people</p>
|
||||
class="relative flex flex-col overflow-hidden panel-lift">
|
||||
<!--/* The wordmark's branch, at watermark weight. */-->
|
||||
<span class="panel-mark -top-3 -right-4 w-24 h-24 rotate-12" aria-hidden="true"></span>
|
||||
|
||||
<ul class="mt-6 space-y-3 text-bakery-800">
|
||||
<div class="panel-head relative px-6 py-6 md:px-8">
|
||||
<p class="label text-bakery-600" th:text="${tier.label}">Small</p>
|
||||
<p th:if="${tier.price}" class="price mt-2 text-bakery-900" th:text="${tier.price}">$24</p>
|
||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
||||
<p th:unless="${tier.price}" class="price mt-2 text-2xl text-bakery-700">Ask us</p>
|
||||
<!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a
|
||||
head count leaves this blank rather than saying it twice. */-->
|
||||
<p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 6–8 people</p>
|
||||
</div>
|
||||
|
||||
<ul class="px-6 py-6 md:px-8 space-y-3 text-bakery-800">
|
||||
<li th:each="row : ${table.rows}" class="flex gap-3">
|
||||
<!--/* A tick, not a bullet: this is a list of what you get, not prose. */-->
|
||||
<svg class="mt-1 h-4 w-4 shrink-0 text-bakery-400" viewBox="0 0 24 24" fill="none"
|
||||
<svg class="mt-1 h-4 w-4 shrink-0 text-bakery-500" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true">
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<img th:src="${photos.of('gallery/' + item + '.webp')}" th:alt="${item}" width="600" height="400" loading="lazy"
|
||||
class="w-full h-56 object-cover transition-transform duration-500 group-hover:scale-105">
|
||||
</div>
|
||||
<h3 class="h4 md:text-2xl text-bakery-800 py-6" th:text="${item}">Cinnamon Rolls</h3>
|
||||
<h3 class="panel-head border-b-0 border-t h4 md:text-2xl text-bakery-800 py-6" th:text="${item}">Cinnamon Rolls</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
<div class="relative w-full overflow-hidden">
|
||||
<div th:replace="~{:: gallery(${product})}"></div>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<h3 class="h4 text-bakery-900 mb-2" th:text="${product.name}">Name</h3>
|
||||
<!--/* 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>
|
||||
|
||||
@@ -39,6 +39,15 @@ body {
|
||||
color: var(--color-bakery-900);
|
||||
}
|
||||
|
||||
/* The mobile menu is a <details>; its default disclosure triangle would sit beside the hamburger. Lost
|
||||
once already in a refactor, which is exactly the kind of thing nobody notices until a phone. */
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* FOCUS. One visible ring for everything you can reach with a keyboard, as an outline rather than a
|
||||
* shadow so it shows up on cream, on white and on the sage bands without needing three variants.
|
||||
|
||||
@@ -1,75 +1,98 @@
|
||||
/*
|
||||
* The public site's own classes — the ones that would be unreadable as a string of utilities in every
|
||||
* template that needs them. The admin's controls are in admin.css.
|
||||
* template that needs them. The admin's controls are in admin.css; the type ladder is in type.css.
|
||||
*
|
||||
* Almost everything here is @utility rather than a plain class in a layer, for two reasons Tailwind v4
|
||||
* enforces: only a registered utility can be @applied by another rule (panel-mark builds on mark), and
|
||||
* only a registered utility can take a variant (`hover:`, `md:`, `file:`).
|
||||
*/
|
||||
|
||||
/* In @layer components, matching the old site. That ordering matters: a utility like `px-4` — which
|
||||
the markup applies alongside `container` on nearly every section — has to win over this, or every
|
||||
gutter on the site silently widens. */
|
||||
/* The exception. In @layer components, matching the old site — and that ordering matters: a utility like
|
||||
`px-4`, which the markup applies alongside `container` on nearly every section, has to win over this,
|
||||
or every gutter on the site silently widens. */
|
||||
@layer components {
|
||||
.container {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The two branch marks of the wordmark, as masks rather than inline SVG.
|
||||
*
|
||||
* They have to take their colour from the surrounding text — sage in the header, cream on the hero
|
||||
* and in the footer — which an <img> cannot do. React solved that by inlining them through svgr,
|
||||
* but these files are 31 KB each and the lockup appears up to three times on a page: inlining them
|
||||
* in server-rendered HTML would add ~190 KB to every response. A mask over `currentColor` keeps the
|
||||
* tinting, keeps the files cacheable, and costs two requests once.
|
||||
*/
|
||||
.mark {
|
||||
background-color: currentColor;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
}
|
||||
.mark-r {
|
||||
-webkit-mask-image: url('/images/logo_R.svg');
|
||||
mask-image: url('/images/logo_R.svg');
|
||||
}
|
||||
.mark-l {
|
||||
-webkit-mask-image: url('/images/logo_L.svg');
|
||||
mask-image: url('/images/logo_L.svg');
|
||||
}
|
||||
/*
|
||||
* THE BRANCH MARKS of the wordmark, as masks rather than inline SVG.
|
||||
*
|
||||
* They have to take their colour from the surrounding text — sage in the header, cream on the hero and in
|
||||
* the footer — which an <img> cannot do. React solved that by inlining them through svgr, but these files
|
||||
* are 31 KB each and the lockup appears up to three times on a page: inlining them in server-rendered
|
||||
* HTML would add ~190 KB to every response. A mask over `currentColor` keeps the tinting, keeps the files
|
||||
* cacheable, and costs two requests once.
|
||||
*/
|
||||
@utility mark {
|
||||
background-color: currentColor;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
/*
|
||||
* The photo strip on a product card scrolls, but a scrollbar across the bottom of a photo is not part
|
||||
* of the design. Hiding it is safe here because the strip is not the only way through the photos: it
|
||||
* snaps, it takes arrow keys, and the arrows and dots are on top of it.
|
||||
*/
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
@utility mark-r {
|
||||
-webkit-mask-image: url('/images/logo_R.svg');
|
||||
mask-image: url('/images/logo_R.svg');
|
||||
}
|
||||
|
||||
@utility mark-l {
|
||||
-webkit-mask-image: url('/images/logo_L.svg');
|
||||
mask-image: url('/images/logo_L.svg');
|
||||
}
|
||||
|
||||
/*
|
||||
* The photo strip on a product card scrolls, but a scrollbar across the bottom of a photo is not part of
|
||||
* the design. Hiding it is safe here because the strip is not the only way through the photos: it snaps,
|
||||
* it takes arrow keys, and the arrows and dots are on top of it.
|
||||
*/
|
||||
@utility no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* The mobile menu is a <details>; its default disclosure triangle would sit next to the hamburger. */
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* SURFACES. Every white box on the site was "bg-white rounded-3xl shadow-xs" written out again, which is
|
||||
* how two of them end up with different corners. `panel` is that box; `panel-lift` is one you can click
|
||||
* or hover, and it lifts.
|
||||
* SURFACES.
|
||||
*
|
||||
* These were plain white boxes with a grey drop shadow — the default card of every web app, on a page
|
||||
* that is otherwise cream, sage and a hand-drawn vine. Three things make them the shop's:
|
||||
*
|
||||
* a warm hairline, so a card has an EDGE and reads as stock rather than as a floating panel;
|
||||
* a shadow tinted with the darkest sage instead of black (see theme.css);
|
||||
* a head that can be tinted cream, the way a printed menu separates the price from what you get.
|
||||
*
|
||||
* `panel-lift` is one you can click, and it rises rather than just darkening.
|
||||
*/
|
||||
@utility panel {
|
||||
@apply bg-white rounded-3xl shadow-xs;
|
||||
@apply bg-white rounded-3xl border border-bakery-200/70;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
@utility panel-lift {
|
||||
@apply panel transition duration-300 hover:shadow-lg;
|
||||
@apply panel transition duration-300;
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-card-lift);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* The cream band on a card: a size and its price, or a photo's caption. */
|
||||
@utility panel-head {
|
||||
@apply bg-bakery-50/70 border-b border-bakery-200/70;
|
||||
}
|
||||
|
||||
/*
|
||||
* The branch, laid into a corner at the weight of a watermark. It is the one mark that is unmistakably
|
||||
* this shop, and at this size and colour it is texture rather than decoration.
|
||||
*/
|
||||
@utility panel-mark {
|
||||
@apply mark mark-l absolute pointer-events-none text-bakery-200/70;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
--color-bakery-800: #37412f;
|
||||
--color-bakery-900: #232b1e;
|
||||
|
||||
/*
|
||||
* Shadows tinted with the darkest sage rather than black. A neutral grey shadow on a cream page reads
|
||||
* as a UI panel floating over a screen; a warm one reads as card stock lying on a counter, which is
|
||||
* what these are meant to be.
|
||||
*/
|
||||
--shadow-card: 0 1px 2px rgba(35, 43, 30, 0.04), 0 10px 30px -18px rgba(35, 43, 30, 0.28);
|
||||
--shadow-card-lift: 0 2px 6px rgba(35, 43, 30, 0.06), 0 20px 44px -22px rgba(35, 43, 30, 0.4);
|
||||
|
||||
--font-sans: 'Raleway', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-adbhashitha: 'AdBhashitha', ui-serif, Georgia, serif;
|
||||
--font-lejour: 'LeJour Script', ui-serif, Georgia, cursive;
|
||||
|
||||
Reference in New Issue
Block a user