styles-only
Minimal template that exercises @pro-laico/styles in isolation: the full back-to-front loop from admin-authored atomic classes to a compiled stylesheet.
A minimal Atomic Payload template that demonstrates the @pro-laico/styles plugin in isolation, and the full back-to-front loop it enables.
What it shows
The template wires up @pro-laico/styles (plus @pro-laico/core, and unocss as a styles peer) and nothing else, so you can see the styling pipeline end to end:
stylesPlugin({ getCached, generateLivePreviewPath })inpayload.config.tsregisters thedesignSet+shortcutSetcollections and thedraftStorage/publishedStorageCSS globals. ThegetCachediscreateCssGetCached()(from@pro-laico/styles/cache); passing it (and noatomicHook) attaches the standalonecssHook, so CSS is processed without the rest of the Atomic runtime.- A
pagescollection houses example blocks (hero,buttonRow,cardGrid,prose,palette). Every visual choice in a block is aClassNameField(the styles plugin's atomic-classes input), not a hard-coded class. The same standalonecssHookis attached topages: on save it walks the document for every*ClassNamevalue (including those nested in blocks), stores them instoredAtomicClasses, and regenerates the stylesheet. getCachedAtomicClassesreads thosestoredAtomicClassesback off the pages, so authored classes become generated CSS with no safelist.- A theme toggle flips the
darkclass to swap every token's light/dark value.
No stylesheet lives in the repo. A designSet (theme) and shortcutSet are authored in the admin, the standalone UnoCSS cssHook collects the classes and compiles the stylesheet, and the frontend renders the blocks with the result.
This template intentionally drops two things that used to be required:
- No
@pro-laico/zap.stylesPluginauto-appends zap's schema extension totypescript.schema(itsregisterTypescriptSchemaoption, default on), sogenerate:typesresolves the designSet field type$refs without the app importing or wiring zap. - No
@pro-laico/fonts. NofontFieldis passed, so the designSet's Fonts tab has no upload fields and the config needs nofontcollection. To enable font uploads, add@pro-laico/fonts, registerfontsPlugin, and passdesignSet: { fontField: fontUploadField() }tostylesPlugin.
Scaffold it
pnpm dlx @pro-laico/create-atomic-payload my-styles --template styles-onlynpx @pro-laico/create-atomic-payload my-styles --template styles-onlyyarn dlx @pro-laico/create-atomic-payload my-styles --template styles-onlyThen set up and run it:
cp .env.example .env # set long PAYLOAD_SECRET + PREVIEW_SECRET (DATABASE_URI is optional)
cp gitignore.template .gitignore
pnpm install
pnpm generate:types # generates src/payload-types.ts + augment
pnpm generate:importmap # populates src/app/(payload)/admin/importMap.js
pnpm devThe demo ships with the SQLite adapter (@payloadcms/db-sqlite) wired to a local file at ./styles-only.db, with no database server required. Swap to Postgres or MongoDB by changing the import + db: call in src/payload.config.ts and installing the matching @payloadcms/db-* package.
Once it's running:
- Open
http://localhost:3000/adminand create your first user. - Go back to
http://localhost:3000. The seed button is now visible: click it to create the design set, shortcut set, and home page. - The page renders, styled by the generated CSS. Edit a block's class names (or a design-set color) in the admin and the page restyles after the save.
How it works
When a page (or the design set / shortcut set) is saved, the cssHook runs the full back-to-front loop:
page (blocks) ─save─▶ cssHook (beforeChange)
├─ collectClassNames(data) → storedAtomicClasses on the page
└─ createCssProcessor → UnoCSS generate(
defaultClasses + getCachedAtomicClasses ◀─ reads every
+ active designSet tokens page's stored
+ active shortcutSet shortcuts ) classes
└─▶ writes layoutCSS to
draft/published storage globals
│
frontend: getCachedSiteCSS → <style> in <head> ◀────────────────┘
RenderBlocks(page.layout) → components apply the stored *ClassName valuesPOST /api/seed creates the shortcut set, the design set, and a home page made of one of each block (auth-gated, idempotent). / loads the home page, injects the generated stylesheet into <head>, and renders the blocks: every utility, shortcut, token color, radius, and prose style was authored in the admin and compiled on save. Live preview is wired for designSet / shortcutSet / pages edits via <LivePreviewListener> + the /next/preview route handler, with local afterChange hooks adding the revalidateTag calls that bust the frontend cache.
What to look at
src/
payload.config.ts # buildConfig — styles plugin, page cssHook, revalidation
collections/
users.ts # auth collection (required by Payload)
pages.ts # createPages(cssHook) — blocks + storedAtomicClasses + revalidation
blocks/
configs.ts # Block configs (ClassNameField-driven) + data types — no React
components.tsx # the matching render components (frontend only)
RenderBlocks.tsx # blockType → component dispatcher
instrumentation.ts # registerPayloadConfig — config for the cache getters
seed/sampleSets.ts # authored designSet + shortcutSet + home page (blocks)
app/
(frontend)/
layout.tsx # injects generated CSS + demo chrome; theme provider
page.tsx # demo toolbar + RenderBlocks(home page)
ThemeToggle.tsx # client light/dark toggle
(payload)/
admin/importMap.js # `pnpm generate:importmap` populates this
api/
seed/route.ts # POST /api/seed → sets + home page
reset/route.ts # POST /api/reset → deletes pages + setsConfigs vs components. blocks/configs.ts is pulled into the Payload config graph (via pages), so it stays free of React / server-only imports. The render components live in blocks/components.tsx and are imported only by the frontend. Keeping them apart is what lets payload generate:types run.