Archived
Catering, and a pure Java/Spring site: Thymeleaf front to back #10
+1
-1
@@ -1,7 +1,7 @@
|
||||
target/
|
||||
# The Tailwind CLI's dependencies. The compiled stylesheet needs no rule of its own: it is written
|
||||
# straight into target/classes/static/css, which is already ignored above.
|
||||
styles/node_modules/
|
||||
src/main/styles/node_modules/
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
|
||||
@@ -12,7 +12,7 @@ there is no JavaScript framework in this repo. The look has not changed through
|
||||
|---|---|
|
||||
| Backend | Spring Boot 4 / Java 25, `com.itsthevine.web` |
|
||||
| Pages | Thymeleaf, `src/main/resources/templates` — **no JavaScript** except one 100-line file for the product-card arrows |
|
||||
| Styling | Tailwind v4, compiled from the templates by the Tailwind CLI into `static/css/site.css`. `styles/` is the whole asset pipeline |
|
||||
| Styling | Tailwind v4, compiled from the templates by the Tailwind CLI into `static/css/site.css`. `src/main/styles` is the whole asset pipeline |
|
||||
| Admin | Thymeleaf forms at `/admin`, behind Authentik |
|
||||
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
||||
| Photos | public MinIO bucket `itsthevine` — **not** in the repo or the image |
|
||||
@@ -120,7 +120,7 @@ history. EXIF (including GPS from phone photos) is stripped by the re-encode.
|
||||
mvn spring-boot:run # http://localhost:8080
|
||||
|
||||
# the stylesheet, while editing templates — watches and recompiles
|
||||
cd styles && npm install && npm run watch
|
||||
cd src/main/styles && npm install && npm run watch
|
||||
```
|
||||
|
||||
`/admin` only exists when `SECURITY_MODE=OIDC`, so a plain local run has the site and no admin. To work
|
||||
|
||||
@@ -127,14 +127,16 @@
|
||||
<!--
|
||||
There is no JavaScript application here any more, so node is present for one reason:
|
||||
Tailwind needs a compiler, and the alternative to it is a hand-written stylesheet.
|
||||
`styles/` is the whole asset pipeline (see styles/site.css).
|
||||
src/main/styles is the whole asset pipeline (see its site.css).
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. -->
|
||||
<workingDirectory>styles</workingDirectory>
|
||||
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. The
|
||||
stylesheet sources live with the rest of the app's source; node_modules has to
|
||||
sit beside them, because that is where Tailwind resolves its own import from. -->
|
||||
<workingDirectory>src/main/styles</workingDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<!--
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
|
||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Contact us</h1>
|
||||
<p class="mt-4 text-bakery-800 max-w-xl mx-auto leading-relaxed">
|
||||
Ask about a cake, a date, or an order for a crowd. We read everything that comes in.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!--/*
|
||||
The form, and beside it the things somebody on this page is usually actually after: when we are open,
|
||||
where we are, and the phone number. They were on the homepage and in the footer but not here, which
|
||||
made this page a form and nothing else.
|
||||
*/-->
|
||||
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
||||
<div class="max-w-2xl mx-auto bg-white p-8 md:p-10 rounded-3xl shadow-xs">
|
||||
<div class="grid gap-8 lg:grid-cols-[1.4fr_1fr] max-w-5xl mx-auto items-start">
|
||||
<div class="bg-white p-8 md:p-10 rounded-3xl shadow-xs">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-2">Get in touch</h2>
|
||||
<p class="text-bakery-700 mb-8">
|
||||
Or call us: <a href="tel:+13097010660" class="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a>
|
||||
@@ -59,6 +68,30 @@
|
||||
<p th:if="${sent}" role="status" class="text-bakery-700">
|
||||
Thanks. Your message is on its way, and we will get back to you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs">
|
||||
<h2 class="font-adbhashitha text-xl text-bakery-900">Our hours</h2>
|
||||
<div class="mt-4 text-bakery-800"
|
||||
th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
|
||||
</div>
|
||||
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs">
|
||||
<h2 class="font-adbhashitha text-xl text-bakery-900">Find us</h2>
|
||||
<div class="mt-4 text-bakery-800"
|
||||
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
|
||||
</div>
|
||||
<div class="rounded-3xl border border-bakery-200 p-6 md:p-8">
|
||||
<h2 class="font-adbhashitha text-xl text-bakery-900">Ordering for a crowd?</h2>
|
||||
<p class="mt-2 text-sm text-bakery-700 leading-relaxed">
|
||||
Goodie boxes, party packages and wedding desserts have their own page, with sizes and prices.
|
||||
</p>
|
||||
<a href="/catering" class="mt-4 inline-block text-bakery-700 hover:text-bakery-900 font-medium
|
||||
underline underline-offset-4 transition-colors">
|
||||
Goodie boxes & catering
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
|
||||
<div>
|
||||
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
|
||||
<address class="not-italic space-y-2">
|
||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
||||
<p><a href="tel:+13097010660" class="hover:text-white transition">(309) 701-0660</a></p>
|
||||
<p class="break-words">
|
||||
<a href="mailto:[email protected]" class="hover:text-white transition">[email protected]</a>
|
||||
</p>
|
||||
</address>
|
||||
<div th:replace="~{fragments/visit :: address('hover:text-white transition')}"></div>
|
||||
<!--/* The hours are the thing people come to a bakery's site for, and the footer is on every
|
||||
page. */-->
|
||||
<div class="mt-4 text-sm text-bakery-200"
|
||||
th:replace="~{fragments/visit :: hours('text-sm text-bakery-200')}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<!--/*
|
||||
The opening hours and the address, in one place.
|
||||
|
||||
They were on the homepage and in the footer already, and the moment the contact page wanted them too
|
||||
that became three copies of the one fact a customer most often comes here for. A shop that changes its
|
||||
Saturday hours should change them once.
|
||||
|
||||
Both fragments take the classes that vary by where they sit — cream text in the footer, sage on white
|
||||
in a card — so the content is shared without the styling being averaged into something that suits
|
||||
neither.
|
||||
*/-->
|
||||
|
||||
<ul th:fragment="hours(rowClass)" class="space-y-3">
|
||||
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||
<span>Tuesday – Friday</span>
|
||||
<span class="font-medium whitespace-nowrap">7:00am – 2:00pm</span>
|
||||
</li>
|
||||
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||
<span>Saturday</span>
|
||||
<span class="font-medium whitespace-nowrap">7:00am – 12:00pm</span>
|
||||
</li>
|
||||
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||
<span>Sunday – Monday</span>
|
||||
<span class="font-medium">Closed</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<address th:fragment="address(linkClass)" class="not-italic space-y-3">
|
||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
||||
<p>
|
||||
<a href="tel:+13097010660" th:class="${linkClass}">(309) 701-0660</a>
|
||||
</p>
|
||||
<p class="break-words">
|
||||
<a href="mailto:[email protected]" th:class="${linkClass}">[email protected]</a>
|
||||
</p>
|
||||
</address>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,9 +3,24 @@
|
||||
<body>
|
||||
<div th:fragment="content" class="bg-bakery-50">
|
||||
|
||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our story</h1>
|
||||
</header>
|
||||
<!--/* The shop's own front, which is what the story is about. Same band as the catering page. */-->
|
||||
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
|
||||
<img th:src="${photos.of('gallery/Outside.webp')}" alt=""
|
||||
class="absolute inset-0 w-full h-full object-cover object-bottom blur-[3px] scale-110">
|
||||
<div class="absolute inset-0 bg-bakery-900/80"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80"></div>
|
||||
<div class="relative container mx-auto px-4 w-full min-w-0">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">Since 2024</p>
|
||||
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Our story</h1>
|
||||
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed">
|
||||
215 E Main Street, in the middle of downtown Princeville.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<header class="container mx-auto px-4 pt-14 pb-2 md:pt-16 text-center"></header>
|
||||
|
||||
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
||||
<div class="max-w-2xl mx-auto space-y-12">
|
||||
@@ -49,6 +64,29 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--/* Somewhere to go after reading: the story is a preamble to an order. */-->
|
||||
<section class="bg-bakery-800 text-white py-16 md:py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl">Come and see us</h2>
|
||||
<p class="mt-4 text-bakery-100 leading-relaxed">
|
||||
We open at 7:00am, Tuesday through Saturday. If you are after something for a date in the
|
||||
diary, it is worth asking early.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<a href="/products"
|
||||
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg">
|
||||
See what we make
|
||||
</a>
|
||||
<a href="/contact"
|
||||
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition">
|
||||
Get in touch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -69,6 +69,32 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--/*
|
||||
Ordering ahead. The catering page exists and the homepage said nothing about it — a customer only
|
||||
found it by reading the nav. Photo on one side, the offer on the other, and the three tables named so
|
||||
the link is worth following.
|
||||
*/-->
|
||||
<section class="py-16 md:py-24 bg-bakery-50">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center max-w-5xl mx-auto">
|
||||
<img th:src="${photos.of('gallery/Sugar Cookies.webp')}" alt="Iced sugar cookies from The Vine"
|
||||
width="800" height="600" loading="lazy"
|
||||
class="w-full h-64 md:h-80 object-cover rounded-3xl shadow-xs">
|
||||
<div>
|
||||
<h2 class="font-adbhashitha text-3xl md:text-4xl text-bakery-900">Ordering for a crowd?</h2>
|
||||
<p class="mt-4 text-bakery-800 leading-relaxed">
|
||||
Goodie boxes for the office, packages for a party, and cakes and desserts for a wedding —
|
||||
with prices, sizes and what each one includes, so you can see where to start.
|
||||
</p>
|
||||
<a href="/catering"
|
||||
class="mt-6 inline-block bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide transition">
|
||||
Goodie boxes & catering
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--/* Visit us */-->
|
||||
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
|
||||
<div class="container mx-auto px-4">
|
||||
@@ -76,32 +102,12 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Our hours</h2>
|
||||
<ul class="space-y-3 text-bakery-800">
|
||||
<li class="flex justify-between gap-4">
|
||||
<span>Tuesday – Friday</span>
|
||||
<span class="font-medium whitespace-nowrap">7:00am – 2:00pm</span>
|
||||
</li>
|
||||
<li class="flex justify-between gap-4">
|
||||
<span>Saturday</span>
|
||||
<span class="font-medium whitespace-nowrap">7:00am – 12:00pm</span>
|
||||
</li>
|
||||
<li class="flex justify-between gap-4">
|
||||
<span>Sunday – Monday</span>
|
||||
<span class="font-medium">Closed</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Find us</h2>
|
||||
<address class="not-italic space-y-3 text-bakery-800">
|
||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
||||
<p>
|
||||
<a href="tel:+13097010660" class="hover:text-bakery-600 underline underline-offset-4">(309) 701-0660</a>
|
||||
</p>
|
||||
<p class="break-words">
|
||||
<a href="mailto:[email protected]" class="hover:text-bakery-600 underline underline-offset-4">[email protected]</a>
|
||||
</p>
|
||||
</address>
|
||||
<div class="text-bakery-800"
|
||||
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our products</h1>
|
||||
<p class="mt-4 text-bakery-800 max-w-2xl mx-auto 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 mx-auto px-4 pb-16">
|
||||
@@ -46,6 +50,29 @@
|
||||
<a href="/products" class="underline underline-offset-4">See everything</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!--/* A catalogue with no way to order from it is a gallery. */-->
|
||||
<section class="bg-bakery-800 text-white py-16 md:py-20">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl">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="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg">
|
||||
Ask about an order
|
||||
</a>
|
||||
<a href="/catering"
|
||||
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition">
|
||||
Ordering for a crowd
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!--/*
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Element defaults: the page background, the body type, the fade-in, and the two small resets the site
|
||||
* needs. Anything with a class name belongs in components.css instead.
|
||||
*/
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
/* Nothing on this site is meant to scroll sideways. */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bakery-50);
|
||||
color: var(--color-bakery-900);
|
||||
font-family: var(--font-sans);
|
||||
overflow-x: hidden;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
body { animation: none; opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-bakery-300);
|
||||
color: var(--color-bakery-900);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* 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. */
|
||||
@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 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 {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "itsthevine-styles",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --minify",
|
||||
"build:css": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --minify",
|
||||
"watch": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "4.3.3",
|
||||
"tailwindcss": "4.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* The site's one stylesheet, compiled by the Tailwind CLI into target/classes/static/css.
|
||||
*
|
||||
* It lives under src/main with the rest of the app's source, and it has to stay in the same directory as
|
||||
* package.json: Tailwind resolves `@import "tailwindcss"` by walking up from the CSS file looking for
|
||||
* node_modules. There is no bundler and no framework here — the site and the admin are both
|
||||
* server-rendered HTML, and this is the whole of the asset pipeline.
|
||||
*
|
||||
* Imported in cascade order: names, then bare elements, then classes.
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "./theme.css";
|
||||
@import "./base.css";
|
||||
@import "./components.css";
|
||||
@import "./admin.css";
|
||||
|
||||
/*
|
||||
* Where Tailwind looks for class names. Every template, and gallery.js — the product-card arrows are
|
||||
* created in script, so their classes are only written down there. A utility exists in the output only if
|
||||
* Tailwind saw it in one of these files, which is why a class name must never be assembled from pieces at
|
||||
* runtime.
|
||||
*/
|
||||
@source "../resources/templates";
|
||||
@source "../resources/static/js";
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* The brand: three typefaces and the sage-and-cream palette. Nothing here draws anything — these are the
|
||||
* names the rest of the CSS and every template are written in.
|
||||
*
|
||||
* Fonts are served by Spring from /fonts rather than bundled, so the stylesheet can name a stable URL and
|
||||
* the browser caches one copy across the whole site.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Raleway';
|
||||
src: url('/fonts/raleway-latin.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'AdBhashitha';
|
||||
src: url('/fonts/AdBhashitha.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'LeJour Script';
|
||||
src: url('/fonts/LeJour-Script.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Sage & Cream. 500 is the signature sage; 600+ are the darker tones that white text can actually
|
||||
sit on (500 on white is only 3.6:1 — too low). */
|
||||
--color-bakery-50: #faf7f0;
|
||||
--color-bakery-100: #f0efe3;
|
||||
--color-bakery-200: #dde0cc;
|
||||
--color-bakery-300: #c3cbae;
|
||||
--color-bakery-400: #a2ae8b;
|
||||
--color-bakery-500: #7c8b6b;
|
||||
--color-bakery-600: #5f6f52;
|
||||
--color-bakery-700: #4a5740;
|
||||
--color-bakery-800: #37412f;
|
||||
--color-bakery-900: #232b1e;
|
||||
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "itsthevine-styles",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --minify",
|
||||
"build:css": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --minify",
|
||||
"watch": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "4.3.3",
|
||||
"tailwindcss": "4.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* The stylesheet for the server-rendered site.
|
||||
*
|
||||
* Compiled by the Tailwind CLI (`npm run build:css`) straight into target/classes/static/css: it is a
|
||||
* source file, not a resource, and the generated stylesheet belongs in the build output rather than in
|
||||
* src/main/resources next to it.
|
||||
*
|
||||
* This directory is the whole asset pipeline, and Tailwind is all that is left of it: the site and the
|
||||
* admin are both server-rendered HTML, so there is no bundler, no framework and one stylesheet. It has
|
||||
* to live here because Tailwind resolves `@import "tailwindcss"` by walking up from the CSS file looking
|
||||
* for node_modules — and node_modules is here.
|
||||
*
|
||||
* @source points Tailwind at every template (public pages and admin alike) and at gallery.js — the
|
||||
* product-card arrows are created in script, so their classes are only written down there. A utility
|
||||
* exists in the output only if Tailwind saw it in one of these files, which is why a class name must
|
||||
* never be assembled from pieces at runtime.
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
@import "./tokens.css";
|
||||
@import "./admin.css";
|
||||
|
||||
@source "../src/main/resources/templates";
|
||||
@source "../src/main/resources/static/js";
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* The brand: the sage-and-cream palette, the three fonts, and the base element rules.
|
||||
*
|
||||
* Shared deliberately. Two stylesheets are compiled from this — the server-rendered site
|
||||
* (src/main/tailwind/site.css, scanning the Thymeleaf templates) and the admin SPA
|
||||
* (frontend/src/index.css, scanning the React source) — and if the tokens were copied into both, the
|
||||
* two halves of the same site would drift a shade apart the first time one of them was edited.
|
||||
*
|
||||
* Fonts are served by Spring from /fonts, not bundled by Vite, so both stylesheets can name the same
|
||||
* URL and the browser caches one copy.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Raleway';
|
||||
src: url('/fonts/raleway-latin.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'AdBhashitha';
|
||||
src: url('/fonts/AdBhashitha.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'LeJour Script';
|
||||
src: url('/fonts/LeJour-Script.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Sage & Cream. 500 is the signature sage; 600+ are the darker tones that white text can actually
|
||||
sit on (500 on white is only 3.6:1 — too low). */
|
||||
--color-bakery-50: #faf7f0;
|
||||
--color-bakery-100: #f0efe3;
|
||||
--color-bakery-200: #dde0cc;
|
||||
--color-bakery-300: #c3cbae;
|
||||
--color-bakery-400: #a2ae8b;
|
||||
--color-bakery-500: #7c8b6b;
|
||||
--color-bakery-600: #5f6f52;
|
||||
--color-bakery-700: #4a5740;
|
||||
--color-bakery-800: #37412f;
|
||||
--color-bakery-900: #232b1e;
|
||||
|
||||
--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;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
/* Nothing on this site is meant to scroll sideways. */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bakery-50);
|
||||
color: var(--color-bakery-900);
|
||||
font-family: var(--font-sans);
|
||||
overflow-x: hidden;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
body { animation: none; opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
@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 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-bakery-300);
|
||||
color: var(--color-bakery-900);
|
||||
}
|
||||
Reference in New Issue
Block a user