a42f1ee8b12f5868de39fa21d9ff95b0186f8466
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a42f1ee8b1 |
Make a card look like it came from the shop, not from a component library
The cards were white boxes with a grey drop shadow and no edge — the default card of every web app,
sitting on a page that is otherwise cream, sage and a hand-drawn vine. Four changes, all of them things
the brand already owns:
- A WARM HAIRLINE. A card now has an edge, so it reads as stock lying on a counter rather than as a panel
floating over a screen.
- A SHADOW IN SAGE, not black — rgba(35,43,30,…), the darkest colour in the palette. Grey shadows are what
made them feel borrowed.
- A CREAM HEAD (`panel-head`). On a catering card the size, price and "About 6–8 people" sit on the tint
with the contents below, the way a printed menu separates what it costs from what you get. Product cards
use the same band for their caption, so the photo reads as mounted on the card instead of being it.
- THE BRANCH, at watermark weight, in the corner of every price card. It is the one mark that is
unmistakably this shop, and it costs nothing: the mask was already there for the wordmark.
The lift is a lift now, two pixels, rather than the shadow just darkening.
TWO THINGS TAILWIND v4 MADE ME FIX ALONG THE WAY. `.mark`, `.mark-l`, `.mark-r` and `.no-scrollbar` were
plain classes in @layer components, and only a registered utility can be @applied by another rule — the
watermark builds on the branch mark, so they became @utility like everything else here. And rewriting that
file dropped the `summary { list-style: none }` reset, which would have put a disclosure triangle beside
the hamburger on every phone; caught it by grepping the compiled CSS rather than by luck.
53 tests green, every class on every page resolves, and the price cards are in the screenshot.
|
||
|
|
ec61b40b12 |
A design pass: the type ladder from the logo, and one name per thing
TYPOGRAPHY, taken from the lockup and walked down to something you can read a paragraph in. "The Vine" in LeJour Script over COFFEEHOUSE + BAKERY in AdBhashitha is the whole brand voice, so: the script stays in the wordmark and nowhere else (it is unreadable at length); AdBhashitha carries h1–h4 and the price, and keeps the wordmark's 0.01em letter-spacing so a heading sits on the same rhythm as the logo above it; `eyebrow` and `label` are the hinge — sans, but letterspaced like the tagline — and then plain Raleway. Sizes are declared once in type.css instead of being re-chosen per template, and none of the type classes set a colour, because the same heading appears in sage on cream and in cream on sage. BUTTONS AND SURFACES have names now. `panel` is the white box that was "bg-white rounded-3xl shadow-xs" written out in nine places (which is how two of them end up with different corners); `panel-lift` is one that reacts. `pill` is one shape in four colourways, which exist because this site puts buttons on cream AND on sage and they cannot be the same colour on both. `chip`/`chip-on` are the products filter. RHYTHM: `section` (py-16 md:py-24) and `band` (py-14 md:py-20, tighter because a coloured strip is already doing the separating) replace nine hand-picked paddings. FOCUS: one visible ring for everything reachable by keyboard, as an outline rather than a shadow so it works on cream, on white and on the sage bands, at zero specificity so a utility can still win. Z-INDEX: written down in base.css, all two of it — 40 the sticky header, 30 the mobile panel, which is a child of the header and so can only ever sit under the bar. Everything else stacks in document order. The point of the comment is that a third layer doesn't get invented at 9999. DROPDOWNS: the admin's category selects were drawing the platform's own arrow in the platform's grey; they get the site's chevron and room for it. 53 tests green. Every class used across all nine pages — five public, four admin — resolves in the compiled stylesheet, so nothing here is styled by a name that doesn't exist. |
||
|
|
5225312a86 |
Give the other pages somewhere to go, and move the stylesheets under src/main
MARKETING, not decoration — each of these was a page that stopped:
- The HOMEPAGE never mentioned catering. A customer found the page by reading the nav. It now has a
section with a photo, what the three tables cover, and a button.
- PRODUCTS was a title and a grid. It gets a line saying what it is ("cakes and decorated cookies are
made to order, so most of what follows started as somebody describing what they wanted") and a closing
band with somewhere to go — an order, or the catering page.
- CONTACT was a form and nothing else. The things somebody on that page actually wants — when we're open,
where we are, the phone number — were on the homepage and in the footer but not there. Now beside the
form, with a card pointing at catering.
- OUR STORY got the shop's own front as a band and an ending, so the story leads somewhere.
- The FOOTER gained the opening hours, which is what people come to a bakery's site for and is now on
every page.
CLEANED UP ON THE WAY. The hours and the address were about to exist in three places, so they are one
fragment (fragments/visit.html) that takes the classes that vary by where it sits — cream in the footer,
sage on white in a card. A shop that changes its Saturday hours should change them once.
STYLES MOVED to src/main/styles, with the rest of the app's source, and split by what each file is for:
theme.css (the fonts and the palette — names, nothing drawn), base.css (bare elements), components.css
(the site's own classes), admin.css (the admin's controls), and site.css as the entry that imports them in
cascade order and declares what Tailwind scans. It was one grab-bag file called tokens.css in a folder at
the repo root. node_modules has to stay beside them — Tailwind resolves `@import "tailwindcss"` by walking
up from the CSS file — so the whole npm project moved together, and the pom, .gitignore and README moved
with it.
53 tests green, `mvn package` builds the stylesheet from its new home, and every class used on all five
pages resolves in the compiled CSS (185/159/175/148/140, none missing).
|
||
|
|
a6596de467 |
An office box is a total, not an assortment — and every size says who it feeds
I had built in an assumption nobody stated, and a ticked list made it a claim: that a $24 box contains
mini muffins AND mini scones AND mini cinnamon rolls. It doesn't. It is twelve items, mixed in sixes,
from those three. The page was describing the offer wrongly — worse than the spreadsheet, which at least
didn't say either way.
The fix keeps one rule for the whole page rather than a mode for this table: a line is something you get,
and a choice within a line is written into its name. The parties and wedding tables already read that way
("Cupcakes or sugar cookies"), so the office table becomes one line — "Any mix of mini muffins, mini
scones and mini cinnamon rolls" — with the size's item count as its cell. Three ticks became one, and the
"6+6" detail moved to the note where the bakery's own minimum already lives: "Baked in sixes, so a dozen
can be six muffins and six scones."
Confirmed, not guessed, for the other two: parties are a cake AND a dozen to hand round; a wedding
package is everything listed. Their ticked lists were already right.
SERVES. "Small" answers nothing for someone ordering for an office, which is the whole question. Columns
gain an optional `serves` — schema, view, a box in the table editor, a line under the price. The office
numbers are estimates from the item counts and are meant to be corrected by the people who pack the
boxes. The party and wedding columns are already named by head count, so theirs stay empty rather than
saying it twice.
53 tests: the office table is one mixed line, its columns say who they feed, and the head-count tables
don't repeat themselves. The blank-cell test builds its own table now, since no seeded line is blank
across any more.
|
||
|
|
15810629c2 |
Say it in the bakery's words, and stop rendering the spreadsheet
The page still read like the source: a grid of cells saying "6+6", "4 dz", "B&G cake", "12 items". That is a note to whoever is baking, not an offer to whoever is buying — and a table of those cells is a picture of the spreadsheet it came from. THE WORDS (V6, guarded on V4/V5's exact text so anything already reworded in the admin is left alone): cells become sentences — "A dozen", "A dozen, in two flavors", "Eighteen, in two or three flavors", "A 6-inch cake", "Two pans", "Four dozen", "Eight inch". Each table gets the line of copy its blurb field always had room for. The parties table had the same offset the weddings one did — quantities on the "Cupcakes" line, an empty "Sugar cookies" line below it — so it is merged the same way, and no line on the page is blank the whole way across any more. Notes read as sentences too: "Everything is baked in sixes, so each item comes in multiples of six." THE LOOK: one card per size instead of a price matrix. The size, the price, then a ticked list of what you get, in a three-up grid that stacks on a phone — so there is now ONE rendering rather than a table for wide screens and cards for narrow ones. A table is still the honest shape for a price matrix, and it is how the bakery keeps these; it just isn't how you sell them. The per-card button went away again after seeing it rendered: all three led to the same place (an enquiry is about the table, not the size) and on the tables whose sizes are headcounts it read "Ask about the 15–20 people". One pill per table now. 53 tests, updated to assert the new wording — including that no cell anywhere still matches "dz", "B&G" or "in cake", and that the price grid is gone. |
||
|
|
ad8ad724a3 |
Make the catering page look like it belongs to the shop
It was correct and plain: a heading, three white cards, a bare table in each. Now it reads like the rest of the site, using only what the site already has. - A short photographic band, same treatment as the homepage hero — blurred cake photo, sage wash, an eyebrow line above the heading. Short: the content is three lines, and 34svh of dark left the title floating in a void. - Prices are the loudest thing in each table, set in the display serif above a small-caps size label, over a ruled header band. Zebra rows below, the line names in the left column. - The branch from the wordmark divides the intro from the tables — it is a mask over currentColor, so it takes the colour of the text around it and costs one span. - Small print and the enquiry link share one tinted card footer. As separate full-width bands they made every card read as four stacked stripes. The link is quiet: three filled buttons down a page shout, and the real call to action belongs once, in a closing sage band with the terms and the phone number. - The blank-cell dash was so faint that a line with no quantities looked like a rendering fault. Checked by rendering it, at 1280 and at 390: hero, tables, the phone's one-card-per-size stack, the closing band. All 197 classes the page uses resolve in the compiled stylesheet, so nothing is styled by accident. Two notes for anyone doing the same: headless Chromium screenshots this site mid-fade unless you pass --virtual-time-budget (the body fades in), and a very tall window makes min-h-screen stretch the flex column, which looks like a gap above the footer and is not one. |
||
|
|
faf7bd9f18 |
One form per row in the admin, and buttons that don't trip its validation
The catalogue screen had a form per control: 467 forms, 464 CSRF tokens, 317 KB of HTML for a page that gets opened on a phone in a bakery. It is now one form per row with several submit buttons — the same `name="do"` pattern the catering table editor already used — which is 89 forms and 206 KB, and less markup to read. Only `save` looks at the text boxes, so moving a row cannot save a half-typed name. The buttons that ignore them carry `formnovalidate`, because the name box is `required` and a browser would otherwise refuse to submit "move down" while that box was empty — a validation error about something the button has nothing to do with. Verified on the running container: 89 forms, move and photo-remove still land, and the seeded catalogue comes back the same after a database reset. |
||
|
|
5045ccc7c8 |
The admin is Thymeleaf too: no JavaScript framework left in the repo
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. |
||
|
|
70af2922f5 |
The site renders itself: Thymeleaf pages, and a catering page among them
The public site was a React SPA. It is now server-rendered Thymeleaf, and the goodie box and catering tables added in the previous commit have a page of their own. The look is unchanged: the templates carry the same Tailwind classes the components did, and every one of the 241 classes the five pages use resolves in the compiled stylesheet. WHAT WENT AWAY. PageMetaController — 148 lines whose only job was to splice per-page <title> and OG tags into one shell with regular expressions, with a test that read the real index.html so that reformatting it failed the build instead of silently breaking the rewriting. A page rendered on the server writes its own head. Also react-router (no client-side routes left), motion, vite-plugin-svgr, and the SPA fallback (platform.web.spa.enabled=false): with the site server-rendered, forwarding a mistyped URL to /index.html would answer with a blank admin shell and a 200 instead of the site's own 404 page. WHAT GOT BETTER ON THE WAY, none of it visible. The category filter is a ?category= link, so every filtered view is a URL you can send someone and a crawler can reach all forty items instead of the twelve the default filter showed. The contact form is a form post: the enquiry is recorded before delivery is attempted, and a refused relay re-renders the page with what the visitor typed still in the boxes. The mobile menu is a <details> — the React version needed four effects to close on navigation, close on Escape, stop the page behind it scrolling, and unmount (a panel parked off-screen still extends the scrollable area, which is how you used to be able to scroll sideways and find the menu); a new document cannot inherit an open menu. THE PUBLIC SITE SHIPS 5 KB OF JAVASCRIPT, and works without it. The product cards are scroll-snap strips, so the photos swipe on a phone and scroll with a trackpad unaided; gallery.js adds the arrows and the dots, and creates them itself rather than having the template render controls that would sit there dead. Tailwind still needs its compiler, so npm remains a BUILD tool: the CLI compiles the templates into static/css/site.css at process-classes (so `spring-boot:run` gets it too), and frontend/ now builds only that stylesheet and the admin. The brand tokens are one file both stylesheets import — the alternative was the shop front and the screen that edits it drifting a shade apart. The stylesheet URL carries ?v=<sha>, because one hand-written CSS file has no content hash and a deploy has to be able to tell a browser that what it cached is stale. The admin is still React and is untouched, apart from losing the router it no longer needs. It is an editor, not content. PlatformContractTest stopped inheriting platform-starter-test's contract and restates it. The shared version asserts that an unknown path forwards to the SPA shell, which is no longer true here, and its test methods are package-private so it cannot be overridden. The platform should decide that assertion from platform.web.spa.enabled — noted in the file. 9 new tests (46 total): every page's real title and og:url, the catalogue and the catering tables in the HTML rather than fetched afterwards, server-side filtering, the 404, and that a crafted ?about= link cannot put words of its own choosing in front of a customer. |