Archived
Catering, and a pure Java/Spring site: Thymeleaf front to back #10
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
target/
|
target/
|
||||||
# The Tailwind CLI's dependencies. The compiled stylesheet needs no rule of its own: it is written
|
# 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.
|
# straight into target/classes/static/css, which is already ignored above.
|
||||||
styles/node_modules/
|
src/main/styles/node_modules/
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
.vscode/
|
.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` |
|
| 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 |
|
| 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 |
|
| Admin | Thymeleaf forms at `/admin`, behind Authentik |
|
||||||
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
||||||
| Photos | public MinIO bucket `itsthevine` — **not** in the repo or the image |
|
| 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
|
mvn spring-boot:run # http://localhost:8080
|
||||||
|
|
||||||
# the stylesheet, while editing templates — watches and recompiles
|
# 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
|
`/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:
|
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.
|
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>
|
<plugin>
|
||||||
<groupId>com.github.eirslett</groupId>
|
<groupId>com.github.eirslett</groupId>
|
||||||
<artifactId>frontend-maven-plugin</artifactId>
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. -->
|
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. The
|
||||||
<workingDirectory>styles</workingDirectory>
|
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>
|
</configuration>
|
||||||
<executions>
|
<executions>
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -5,10 +5,19 @@
|
|||||||
|
|
||||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
<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>
|
<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>
|
</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="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>
|
<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">
|
<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>
|
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">
|
<p th:if="${sent}" role="status" class="text-bakery-700">
|
||||||
Thanks. Your message is on its way, and we will get back to you.
|
Thanks. Your message is on its way, and we will get back to you.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,13 +20,11 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
|
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
|
||||||
<address class="not-italic space-y-2">
|
<div th:replace="~{fragments/visit :: address('hover:text-white transition')}"></div>
|
||||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
<!--/* The hours are the thing people come to a bakery's site for, and the footer is on every
|
||||||
<p><a href="tel:+13097010660" class="hover:text-white transition">(309) 701-0660</a></p>
|
page. */-->
|
||||||
<p class="break-words">
|
<div class="mt-4 text-sm text-bakery-200"
|
||||||
<a href="mailto:[email protected]" class="hover:text-white transition">[email protected]</a>
|
th:replace="~{fragments/visit :: hours('text-sm text-bakery-200')}"></div>
|
||||||
</p>
|
|
||||||
</address>
|
|
||||||
</div>
|
</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>
|
<body>
|
||||||
<div th:fragment="content" class="bg-bakery-50">
|
<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">
|
<!--/* The shop's own front, which is what the story is about. Same band as the catering page. */-->
|
||||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our story</h1>
|
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
|
||||||
</header>
|
<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="container mx-auto px-4 pb-16 md:pb-24">
|
||||||
<div class="max-w-2xl mx-auto space-y-12">
|
<div class="max-w-2xl mx-auto space-y-12">
|
||||||
@@ -49,6 +64,29 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -69,6 +69,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 */-->
|
<!--/* Visit us */-->
|
||||||
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
|
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
|
||||||
<div class="container mx-auto px-4">
|
<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="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">
|
<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>
|
<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">
|
<div th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
|
||||||
<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>
|
</div>
|
||||||
<div class="bg-white rounded-3xl p-6 md:p-8">
|
<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>
|
<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">
|
<div class="text-bakery-800"
|
||||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
|
||||||
<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>
|
</div>
|
||||||
</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">
|
<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>
|
<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>
|
</header>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 pb-16">
|
<div class="container mx-auto px-4 pb-16">
|
||||||
@@ -46,6 +50,29 @@
|
|||||||
<a href="/products" class="underline underline-offset-4">See everything</a>.
|
<a href="/products" class="underline underline-offset-4">See everything</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</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