From 37c517d2ffee7b81d1292f2ea15b58cc932245c4 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 09:24:41 +0200 Subject: [PATCH 1/2] feat: generate the editor's palette, type scale and radii from @theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit theme.json held the palette by hand, and @theme read it back through the presets; the two had to be kept in step, and a colour changed in app.css never reached the editor. apiary takes the other road: app.css is the source, and the build writes theme.json. Both themes now work the same way. - @theme is `static` and holds the colours, the type scale (xs to 7xl) and the radii with concrete values; wordpressThemeJson copies them into theme.json, whose base keeps no palette and no font sizes. - A :root rule points the colour utilities at the WordPress presets, so a palette edited in the Site Editor still reaches the theme. - Inter stays in theme.json: its @font-face can only be declared there, so the fonts are left out of the generation (disableTailwindFonts). The font size slugs follow Tailwind now (sm, base, 4xl…) instead of small, medium, large and x-large. --- resources/assets/css/app.css | 61 +++++++++++++++++++++++++----- theme.json | 72 ------------------------------------ vite.config.js | 27 +++++++++++++- 3 files changed, 77 insertions(+), 83 deletions(-) diff --git a/resources/assets/css/app.css b/resources/assets/css/app.css index c21ffcd..49a4c60 100644 --- a/resources/assets/css/app.css +++ b/resources/assets/css/app.css @@ -2,16 +2,57 @@ @source "../../views"; -@theme { - --color-primary: var(--wp--preset--color--primary, #ff5334); - --color-primary-vivid: var(--wp--preset--color--primary-vivid, #fb196d); - --color-secondary: var(--wp--preset--color--secondary, #ff8c12); - --color-accent: var(--wp--preset--color--accent, #feb017); - --color-foreground: var(--wp--preset--color--foreground, #1d142a); - --color-muted: var(--wp--preset--color--muted, #6b7280); - --color-surface: var(--wp--preset--color--surface, #fafafa); - --color-surface-alt: var(--wp--preset--color--surface-alt, #f5f3ff); - --color-border: var(--wp--preset--color--border, #e5e7eb); +@theme static { + --color-primary: #ff5334; + --color-primary-vivid: #fb196d; + --color-secondary: #ff8c12; + --color-accent: #feb017; + --color-foreground: #1d142a; + --color-muted: #6b7280; + --color-surface: #fafafa; + --color-surface-alt: #f5f3ff; + --color-background: #ffffff; + --color-border: #e5e7eb; + /* Type scale offered in the editor */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + --text-6xl: 3.75rem; + --text-7xl: 4.5rem; + /* Corner radii */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; +} + +/* + * The build copies the @theme colours, sizes and radii above into theme.json. + * They are `static`, so each one reaches it even when no utility uses it, and + * concrete: a preset defined through its own variable is a cycle, and CSS drops + * it. The utilities then read the WordPress colour presets, so a palette edited + * in the Site Editor still reaches the theme. + */ +@layer base { + :root { + --color-primary: var(--wp--preset--color--primary, #ff5334); + --color-primary-vivid: var(--wp--preset--color--primary-vivid, #fb196d); + --color-secondary: var(--wp--preset--color--secondary, #ff8c12); + --color-accent: var(--wp--preset--color--accent, #feb017); + --color-foreground: var(--wp--preset--color--foreground, #1d142a); + --color-muted: var(--wp--preset--color--muted, #6b7280); + --color-surface: var(--wp--preset--color--surface, #fafafa); + --color-surface-alt: var(--wp--preset--color--surface-alt, #f5f3ff); + --color-background: var(--wp--preset--color--background, #ffffff); + --color-border: var(--wp--preset--color--border, #e5e7eb); + } } /* WordPress content blocks */ diff --git a/theme.json b/theme.json index 3ecf6da..e227d4e 100644 --- a/theme.json +++ b/theme.json @@ -4,60 +4,6 @@ "customTemplates": [], "settings": { "appearanceTools": true, - "color": { - "palette": [ - { - "slug": "primary", - "color": "#ff5334", - "name": "Primary" - }, - { - "slug": "primary-vivid", - "color": "#fb196d", - "name": "Primary Vivid" - }, - { - "slug": "secondary", - "color": "#ff8c12", - "name": "Secondary" - }, - { - "slug": "accent", - "color": "#feb017", - "name": "Accent" - }, - { - "slug": "foreground", - "color": "#1d142a", - "name": "Foreground" - }, - { - "slug": "muted", - "color": "#6b7280", - "name": "Muted" - }, - { - "slug": "surface", - "color": "#fafafa", - "name": "Surface" - }, - { - "slug": "surface-alt", - "color": "#f5f3ff", - "name": "Surface Alt" - }, - { - "slug": "background", - "color": "#ffffff", - "name": "Background" - }, - { - "slug": "border", - "color": "#e5e7eb", - "name": "Border" - } - ] - }, "custom": { "spacing": { "small": "max(1.25rem, 5vw)", @@ -108,24 +54,6 @@ } ] } - ], - "fontSizes": [ - { - "size": "var(--text-sm)", - "slug": "small" - }, - { - "size": "var(--text-base)", - "slug": "medium" - }, - { - "size": "var(--text-4xl)", - "slug": "large" - }, - { - "size": "var(--text-5xl)", - "slug": "x-large" - } ] }, "layout": { diff --git a/vite.config.js b/vite.config.js index 6b2b01b..286f2aa 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,6 @@ import { defineConfig } from "vite"; import laravel, { refreshPaths } from 'laravel-vite-plugin'; -import { wordpressPlugin } from '@roots/vite-plugin'; +import { wordpressPlugin, wordpressThemeJson } from '@roots/vite-plugin'; import { globSync } from 'glob'; import path from 'path'; import tailwindcss from '@tailwindcss/vite'; @@ -93,6 +93,31 @@ export default defineConfig({ plugins: [ tailwindcss(), laravel(getThemeConfig()), + wordpressThemeJson({ + baseThemeJsonPath: './theme.json', + // Inter needs its @font-face, which only theme.json can declare + disableTailwindFonts: true, + fontSizeLabels: { + xs: 'Extra Small', + sm: 'Small', + base: 'Medium', + lg: 'Large', + xl: 'Extra Large', + '2xl': '2X Large', + '3xl': '3X Large', + '4xl': '4X Large', + '5xl': '5X Large', + '6xl': '6X Large', + '7xl': '7X Large', + }, + borderRadiusLabels: { + sm: 'Small', + md: 'Medium', + lg: 'Large', + xl: 'Extra Large', + '2xl': '2X Large', + }, + }), ...(hasBlocks ? [wordpressPlugin()] : []), { name: "blade", From a59e941c95232090ee026b51331261c4b360608a Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 09:25:55 +0200 Subject: [PATCH 2/2] docs: say where the design tokens live and how to keep one out of Tailwind --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 3c3e607..d4456f7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,26 @@ composer create-project pollora/pollora my-project php artisan pollora:make-theme my-theme ``` +## Design tokens + +The design lives in the `@theme static` block of `resources/assets/css/app.css`: +colours, type scale and radii, with concrete values. `npm run build` writes +them into the `theme.json` the editor reads +(`public/build/theme//assets/theme.json`), so the page and the editor +always offer the same palette and sizes. + +- Change a token in `app.css`, not in `theme.json`: the root `theme.json` is + only the base (layout, fonts, block styles), and a slug defined there wins + over `@theme`. +- The font is the exception: Inter needs a `fontFace` declaration, which only + `theme.json` can hold, so fonts are declared there and `vite.config.js` turns + their generation off (`disableTailwindFonts`). The same option exists for + colours, font sizes and radii. +- Spacing and layout widths are not generated: set them in `theme.json`. + +See [Theme.json and Vite Build Integration](https://pollora.dev/theming/theme-structure/) +for the details. + ## Contributing ### Development Setup