THEMES

How themes work

Themes

The storefront's look is data, not code. A theme row holds nine colour tokens, a Google Fonts family, and the header menu. Exactly one theme is active; activating another deactivates it in the same transaction.

The admin's Themes view is a gallery of design cards — each card renders the real storefront preview in that theme's tokens, scaled into the card, so the designs are compared visually rather than by name. Clicking a card opens its configuration; ← ALL THEMES returns to the gallery. + NEW THEME starts from the currently live design, so a new one is a variation rather than a blank slate.

A theme owns everything that is art direction rather than catalogue data:

In the theme Not in the theme
Palette, display font Products, categories, stock
Header menu Filters (shared across designs)
Home banner — image, copy, buttons Orders, customers
Footer — tagline, columns, small print

Theme 1 is the original prototype design.

Sharing themes

Writing a theme from scratch is documented in THEMING.md — the file format, every field limit, what each colour token paints, and what to check before handing one to someone else.

A theme is self-contained — colours, font, menu, banner and footer, with no reference to products or anything else store-specific — so it moves between stores as a file.

GET  /admin/themes/:id/export   -> a .theme.json file
POST /admin/themes/import       <- that file, into any store

EXPORT on a theme card downloads <name>.theme.json; IMPORT THEME in the gallery reads one back. The file carries a format marker so a stray JSON file is rejected with a clear message rather than a validation dump, and every field is validated on the way in.

An imported theme always arrives inactive, so bringing one in never changes what customers see. A name clash suffixes rather than overwrites: Theme 1 -> Theme 1 (imported) -> Theme 1 (imported 2).

Banner and footer images travel as URLs. Within one deployment they resolve straight away because all stores share an asset store; a theme imported into a different deployment keeps its colours, font and layout but falls back to placeholder imagery. Hero and footer columns are nullable, so themes created before those existed fall back to defaults rather than breaking.

GET   /theme                        # public — what the storefront renders with
GET   /admin/themes                 POST /admin/themes
PUT   /admin/themes/:id             PATCH /admin/themes/:id/activate
DELETE /admin/themes/:id            # refuses while the theme is active

Colour tokens reach the browser as CSS variables holding RGB triplets (--c-ink: 27 23 20), because Tailwind's /opacity modifiers — bg-ink/50 and friends — cannot work against a plain hex variable. The layout injects them into :root and loads the font, so switching a theme restyles the shop on the next request with no rebuild.

Menu hrefs are validated as relative paths; an absolute URL is rejected to avoid turning the store header into an open redirect.