Archived
build-and-publish / build (pull_request) Successful in 2m0s
The last React went with this. /admin and /admin/catering are pages of forms; every write is a POST and a redirect back, so the back button and reload do what they look like they do, a double-tap cannot repeat an upload, and there is no client-side state to lose — a reload is always the truth. The /api/admin/** endpoints went too: they existed for the React screen, and their logic now lives in Catalogue (extracted from the two deleted JSON controllers) and CateringMenu, which the pages call. THE TABLE EDITOR IS THE INTERESTING PART, because a catering table cannot be edited a field at a time — a column heading, its price and the entries beneath it only mean anything together. One form holds the whole table and every button submits it; `name="do"` says which was pressed and its value carries the position (`remove-column:2`). "Add a column" therefore arrives with every cell the editor has typed, adds the column to what arrived plus an empty entry on every line, and re-renders. Nothing typed is lost, and only Save writes — so a half-built table with a blank heading never reaches the live page. A failed save comes back the same way, with the work still in the form and the reason above it; a redirect would throw the work away and leave them guessing which cell the message was about. Spring binds `lines[2].values[1]` into the right cell, which flat repeated parameters could not promise. Reordering moved to the server, where it always belonged: the browser used to compute the new order and send the whole list back, and now "move this up" arrives as an action. Same for arranging photos — one endpoint takes the key and -1/1/0 (earlier, later, remove), because those three buttons are the same edit. frontend/ became styles/: node, Tailwind and nothing else. It exists because Tailwind needs a compiler and the alternative is a hand-written stylesheet; there is no bundler and no framework. The admin's controls are @utility classes (v4 will only let you @apply a registered utility, and only a utility can take the `file:` variant the photo pickers use) — the same buttons the React screen had, from the same class strings it composed. Also fixed .gitignore, which still named frontend/: with styles/ unlisted, `git add -A` staged 1,626 files of node_modules. Verified against a running container, not only in tests: pressing "+ Column" returns the draft with an unsaved cell intact, a new column and a matching new entry on the line, "Not saved yet" — and the live page unchanged; Save then writes both columns with the price parsed from "48". Renaming and moving an item land on the products page. Deleting a category that is in use is refused with the sentence naming it. Removing the only photo of an item is refused, and that button is already disabled in the page. 51 tests (10 new): the form binding, the flash on success and on refusal, a structural button writing nothing, and the whole admin surface closed to anonymous visitors. PlatformContractTest's routing assertion now says what is true — an unknown path 404s, and so does /admin when no identity provider is configured, because AdminController only exists under OIDC. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
141 lines
4.2 KiB
CSS
141 lines
4.2 KiB
CSS
/*
|
|
* 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);
|
|
}
|