@woodylab/payload
v0.0.203
Published
**UIMS — User Interface Management System.** Sistema di rendering della UI guidato dal CMS per applicazioni **Payload CMS 3 + Next.js (App Router)**.
Readme
@woodylab/payload
UIMS — User Interface Management System. Sistema di rendering della UI guidato dal CMS per applicazioni Payload CMS 3 + Next.js (App Router).
La struttura, il layout, gli stili e il tema delle pagine sono definiti in Payload (collections, globals, blocks, ViewModes); il pacchetto li risolve e li renderizza dinamicamente in componenti React. I componenti sono privi di logica (dumb): non eseguono fetch né business logic e renderizzano esclusivamente ciò che il motore UIMS fornisce loro.
Documentazione completa del pacchetto: overview, requisiti e guida all'integrazione.
Indice
- Requisiti
- Installazione
- Entrypoint e design subpath-only
- 1. Stili Tailwind
- 2. Configurazioni Payload
- 3. uimsContext
- 4. Rendering: globals e pagine
- Riferimento rapido degli import
Requisiti
Il pacchetto è ESM-only e si integra in un'applicazione Payload + Next moderna. Le versioni seguono l'ecosistema Payload, che ne determina i vincoli.
| Peer dependency | Versione |
| --- | --- |
| payload | ^3.54.0 |
| @payloadcms/ui | ^3.54.0 |
| @payloadcms/richtext-lexical | ^3.54.0 |
| next | ^15.2.3 |
| react / react-dom | ^19 |
Installazione
npm install @woodylab/payloadLe peer dependencies sono in genere già presenti in un progetto Payload + Next.
Entrypoint e design subpath-only
| Import | Contenuto |
| --- | --- |
| @woodylab/payload/config | Schema Payload: collections, globals, blocks, fields, hooks, override dei plugin |
| @woodylab/payload/uims | Motore: createUIMSContext, uimsResolvers, resolver e mapper |
| @woodylab/payload/react | Componenti React (dumb), uimsComponents, provider eventi |
| @woodylab/payload/utils | Helper: generateCssVars, getCachedGlobal, … |
| @woodylab/payload/uimsManifest | Dizionario di classi pre-generato (default export) |
| @woodylab/payload/seeders | Seeder (syncStylesToDB, syncViewModesToDB, seedEvents) + viewModeDefaults |
Il pacchetto è subpath-only: non espone un export root .. Si tratta di una scelta deliberata. Un barrel di root unirebbe codice server (la configurazione Payload, che importa payload, il database e API Node) e codice client (i componenti React marcati "use client") nello stesso modulo, con conseguenti conflitti di risoluzione delle dipendenze e violazione dei confini server/client di RSC. I subpath isolano ogni responsabilità.
Importare sempre dai subpath pubblici elencati sopra. I percorsi interni (
@woodylab/payload/dist/...) sono incapsulati dal campoexportse non vanno usati.
Quick start
L'integrazione lato consumer si articola in tre passi, dettagliati di seguito:
- Stili Tailwind — inclusione della safelist generata dal pacchetto e iniezione delle variabili del tema (§1).
- Config Payload — registrazione di collections, globals e plugin form (§2).
- uimsContext — creazione del contesto che collega componenti, stili e resolver, e suo utilizzo in fase di rendering (§3–§4).
1. Stili Tailwind
UIMS adotta Tailwind CSS v4 con due specificità:
- le classi strutturali dei componenti non compaiono nei file
.tsxdel progetto consumer (provengono dal CMS a runtime) e vanno pertanto preservate tramite safelist; - i colori del tema sono modificabili dal CMS (global
theme) e iniettati come variabili CSS a runtime.
1.1 La safelist e il manifest (lato pacchetto)
La build del pacchetto (npm run build → step build:manifest) genera in dist/:
| File | Cos'è | Come si usa |
| --- | --- | --- |
| uims-safelist.json | Array di tutte le classi Tailwind ammesse | Letto da Tailwind via @source |
| uimsManifest.js | Dizionario { classe: classe } (default export) | Passato a createUIMSContext come stylesDict (§3) |
uimsManifest— natura e scopo. File generato dalla build: un insieme di classi Tailwind a supporto del bootstrap dell'applicazione, prima che il progetto definisca la propria lista di stili. Lo stesso insieme alimenta tre elementi: (1) il context (stylesDict), (2) la safelist Tailwind, (3) il seeder degli stili. Non corrisponde all'importMapdi Payload: è una mappa di classi CSS, non di componenti dell'admin.
Il dizionario copre la palette semantica (primary, secondary, accent, info, success, warning, danger, dark, light, foreground) con shade -10…-90 e sotto-varianti -light-10…40 / -dark-10…40, oltre a spaziature, flex/grid, posizionamento, trasformazioni, transizioni, animazioni tailwindcss-animate e stati data-[state=…] di Radix. La safelist è inclusa nel pacchetto e non richiede generazione manuale.
1.2 Configurazione del CSS (lato consumer)
Nel file CSS globale dell'applicazione (es. src/app/(frontend)/styles.css):
@import "tailwindcss";
@plugin "tailwindcss-animate";
/* Preserva tutte le classi strutturali del pacchetto UIMS.
La profondità del path relativo va adattata alla posizione del file CSS. */
@source "../../../node_modules/@woodylab/payload/dist/uims-safelist.json";
@theme {
/* Font */
--font-heading: var(--font-heading);
--font-text: var(--font-text);
/* Mappa i token semantici sulle variabili runtime (--tw-source-*) iniettate dal CMS.
Esempio completo per "primary"; lo schema si ripete per le altre famiglie. */
--color-primary: var(--tw-source-primary);
--color-primary-10: var(--tw-source-primary-10);
/* … -20 … -90 … */
--color-primary-light-10: var(--tw-source-primary-light-10);
/* … -light-20 … -light-40 … */
--color-primary-dark-10: var(--tw-source-primary-dark-10);
/* … -dark-20 … -dark-40 … */
/* Lo stesso blocco si ripete per: secondary, accent, info, success, warning, danger, dark, light */
--color-foreground: var(--tw-source-foreground);
}
@layer base {
:root { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
body {
background-color: var(--color-foreground);
color: var(--color-dark);
font-family: var(--font-text);
}
h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); }
}Note:
@sourceistruisce Tailwind v4 a includere le classi del JSON anche quando non compaiono nel codice. Il path è relativo al file CSS; in un monorepo con il pacchetto in locale diventa, ad esempio,../../../@woodylab/payload/dist/uims-safelist.json.- Il blocco
@themenon assegna colori fissi: mappa ogni token (--color-primary) su una variabile runtime (--tw-source-primary), popolata dal CMS al passo successivo.
1.3 Iniezione delle variabili a runtime (layout.tsx)
Le variabili --tw-source-* sono generate dal global theme tramite generateCssVars e iniettate nell'<head>:
import { getPayload } from 'payload'
import payloadConfig from '@payload-config'
import { generateCssVars, getCachedGlobal } from '@woodylab/payload/utils'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const payload = await getPayload({ config: payloadConfig })
const theme = await getCachedGlobal(payload, 'theme') // 1. tema dal CMS
const cssVars = generateCssVars(theme) // 2. stringa CSS --tw-source-*
return (
<html lang="en" data-theme="light">
<head>
{/* 3. iniezione delle variabili prima del body */}
<style dangerouslySetInnerHTML={{ __html: cssVars }} />
</head>
<body className="bg-foreground">{children}</body>
</html>
)
}Il tema chiaro/scuro è pilotato dall'attributo data-theme su <html> (light / dark).
2. Configurazioni Payload
Lo schema di dominio si importa da /config, mantenendolo separato dal bundle frontend.
uimsCollections→ array concollectionStyles,collectionViewModes,collectionBlocks,collectionSections,collectionViews,collectionMenus,collectionEvents,collectionPages.uimsGlobals→ array conglobalTheme,globalSettings,globalFooter,globalHeader.uimsFormOverrides/uimsFormSubmissionOverrides→ override per@payloadcms/plugin-form-builder.
import { buildConfig } from 'payload'
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
import {
uimsCollections,
uimsGlobals,
uimsFormOverrides,
uimsFormSubmissionOverrides,
} from '@woodylab/payload/config'
export default buildConfig({
collections: [
...uimsCollections,
// …collection applicative (Users, Media, …)
],
globals: [...uimsGlobals],
plugins: [
formBuilderPlugin({
formOverrides: uimsFormOverrides as any,
formSubmissionOverrides: uimsFormSubmissionOverrides as any,
fields: {},
}),
],
// …resto della configurazione (db, editor, secret, admin, …)
})Le singole entità sono importabili anche individualmente (
collectionViewModes,globalTheme, …), qualora si preferisca comporre gli array manualmente.
Seeding (opzionale)
I seeder del pacchetto popolano il CMS con i dati di base. L'ordine di esecuzione (tipicamente in onInit) è il seguente:
import uimsManifest from '@woodylab/payload/uimsManifest'
import { syncStylesToDB, syncViewModesToDB, seedEvents, viewModeDefaults } from '@woodylab/payload/seeders'
// in onInit(payload):
await syncStylesToDB(payload, uimsManifest) // 1. Styles
await syncViewModesToDB(payload, viewModeDefaults) // 2. ViewModes
await seedEvents(payload, eventNames) // 3. Events3. uimsContext
L'uimsContext collega l'istanza Payload, i componenti React, il dizionario degli stili e i resolver; espone resolve() per trasformare i dati del CMS in elementi renderizzabili.
3.1 La firma
createUIMSContext(
payload: BasePayload,
uiComponents: Record<string, any>,
stylesDict: Record<string, string> = {},
resolvers: Record<string, ResolverFunction> = {},
): Promise<UIMSContext>| Argomento | Origine | Ruolo |
| --- | --- | --- |
| payload | getPayload({ config }) | Istanza Payload per la lettura dei dati |
| uiComponents | uimsComponents da /react | Mappa tipo → componente React |
| stylesDict | uimsManifest da /uimsManifest | Dizionario classi pre-generato |
| resolvers | uimsResolvers da /uims | Logica di risoluzione per tipo di blocco |
Il context espone resolve(block), che restituisce { Component, ...props }. A partire dalla 0.0.197 i resolver di default (uimsResolvers) sono inclusi automaticamente anche in assenza del parametro (merge { ...uimsResolvers, ...resolvers }).
3.2 Configurazione di riferimento (uimsContext.ts)
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { uimsComponents } from '@woodylab/payload/react'
import uimsManifest from '@woodylab/payload/uimsManifest'
import { createUIMSContext, uimsResolvers } from '@woodylab/payload/uims'
const payload = await getPayload({ config: configPromise })
const uiComponents = { ...uimsComponents } // estendibile con componenti applicativi
const mergedStyles = { ...uimsManifest } // il manifesto fornisce gli stili anche in fase di init
const resolvers = { ...uimsResolvers } // estendibile con resolver applicativi
export const UIMS = await createUIMSContext(payload, uiComponents, mergedStyles, resolvers)Nota sul manifest.
@woodylab/payload/uimsManifestè un artefatto di build (presente solo indist/). In un'applicazione installata da npm la risoluzione avviene tramiteexports. In un monorepo che consuma i sorgenti tramite alias, il nome va mappato sul file generato con un path TS dedicato:// tsconfig.json → compilerOptions.paths "@woodylab/payload/uimsManifest": ["./@woodylab/payload/dist/uimsManifest"]
3.3 Estendere il context
uiComponents, mergedStyles e resolvers sono oggetti estendibili: è possibile aggiungere le proprie voci senza modificare il pacchetto.
const uiComponents = { ...uimsComponents, Hero: MyHero }
const resolvers = { ...uimsResolvers, myType: myCustomResolver }3.4 Utilizzo del context
resolve() accetta un global o un blocco e restituisce { Component, ...props }. I pattern completi per global e pagine sono descritti nella §4.
3.5 Interattività ed eventi
I componenti interattivi (azioni, dialog, form, accordion) richiedono il provider eventi; in sua assenza l'integrazione lato consumer è incompleta. L'orchestrazione si articola in tre file.
// app-events.ts — non è un client component, così è condivisibile tra seeder (server) e wrapper (client)
import { uimsEventHandlers } from '@woodylab/payload/react'
export const customHandlers = {
// handler applicativi, es: 'alertDialog:confirm': async (event) => { … }
}
export const mergedHandlers = { ...uimsEventHandlers, ...customHandlers }// uimsEventsWrapper.tsx — wrapper client che monta il provider
'use client'
import { UIMSEventProvider } from '@woodylab/payload/react'
import { mergedHandlers } from './app-events'
const defaultHandler = async (event: any) => console.log('Evento non mappato:', event)
export function UIMSEventsWrapper({ children }: { children: React.ReactNode }) {
return (
<UIMSEventProvider handlers={mergedHandlers} defaultHandler={defaultHandler}>
{children}
</UIMSEventProvider>
)
}// layout.tsx — il provider avvolge l'albero dell'app
<UIMSEventsWrapper>{/* header, children, footer */}</UIMSEventsWrapper>Note:
UIMSEventProvideraccettahandlers(mappatipo evento → handler) e undefaultHandler(fallback per eventi non mappati).- Il provider è un client component e va isolato in un wrapper dedicato (
'use client'), così da poter essere utilizzato dallayout.tsxlato server. - Gli stessi
mergedHandlersalimentano il seeder degli eventi:seedEvents(payload, Object.keys(mergedHandlers)). - Nei componenti,
useUIMSEvents()esponedispatch(event)per l'invio delle azioni.
4. Rendering: globals e pagine
Prerequisito. Né un global né una pagina sono renderizzabili senza un context
UIMScorrettamente inizializzato (§3): sono necessaristylesDict(il manifest), iresolverse iuiComponents. In loro assenzaUIMS.resolve()non è in grado di mappare i tipi e i componenti risultano vuoti.
| | Global | Pagina |
| --- | --- | --- |
| Fonte | getCachedGlobal(payload, slug) | payload.find({ collection, where: { slug } }) |
| resolve restituisce | un singolo { Component, ...props } | un array di blocchi (layout) risolti |
| Rendering | <Component {...props} /> diretto | <LayoutRenderer resolvedLayout={…} /> |
4.1 Global + resolve
Un global UIMS (es. header, footer) va prima registrato in configurazione (uimsGlobals — §2) e popolato dal CMS. A runtime viene letto, risolto e reso disponibile come componente:
// app/(frontend)/layout.tsx
import { getPayload } from 'payload'
import payloadConfig from '@payload-config'
import { getCachedGlobal } from '@woodylab/payload/utils'
import { UIMS } from '@/uimsContext'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const payload = await getPayload({ config: payloadConfig })
const header = await getCachedGlobal(payload, 'header')
const footer = await getCachedGlobal(payload, 'footer')
const { Component: Header, ...headerProps } = await UIMS.resolve(header)
const { Component: Footer, ...footerProps } = await UIMS.resolve(footer)
return (
<body>
<Header {...headerProps} />
{children}
<Footer {...footerProps} />
</body>
)
}getCachedGlobal(payload, slug)recupera il global con tutte le sue property.UIMS.resolve(global)applica il resolver associato alglobalTypee restituisce{ Component, ...props }(lepropsincludono gli eventualislots).- Il componente viene reso direttamente:
<Header {...headerProps} />.
Il global
themecostituisce un'eccezione: non viene renderizzato ma passato agenerateCssVarsper le variabili CSS (§1.3).
4.2 Pagina + resolve
Una pagina è un documento di una collection (es. pages) con uno slug e un campo layout costituito da un array di blocchi UIMS.
UIMS fornisce una collection pages pronta all'uso (collectionPages, inclusa in uimsCollections — §2): il relativo campo layout impiega i blocchi sezione UIMS. In alternativa è possibile gestire la collection lato consumer, purché il campo layout utilizzi uimsSections:
import { uimsSections } from '@woodylab/payload/config'
// campo `layout` della collection Pages:
{
name: 'layout',
type: 'blocks',
blocks: uimsSections, // = blockSectionInline + blockSectionReference
}I blocchi dei template Payload (
CallToAction,Content, …) non sono risolvibili da UIMS: il loroblockTypenon dispone di un resolver e non viene renderizzato. Vanno sostituiti conuimsSections.
Il route dinamico di Next mappa lo slug → documento → blocchi risolti:
// app/(frontend)/[slug]/page.tsx
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { UIMS } from '@/uimsContext'
import { LayoutRenderer } from '@woodylab/payload/react'
export default async function Page({ params }: { params: Promise<{ slug?: string }> }) {
const { slug = 'home' } = await params
const payload = await getPayload({ config: configPromise })
// 1. Recupero della pagina per slug (depth elevato = relazioni popolate a fondo)
const result = await payload.find({
collection: 'pages',
depth: 99,
limit: 1,
pagination: false,
where: { slug: { equals: slug } },
})
const page = result.docs?.[0] || null
// 2. Risoluzione di ogni blocco del layout
const layout = page?.layout
const resolvedLayout = Array.isArray(layout)
? await Promise.all(layout.map((block: any) => UIMS.resolve(block)))
: []
// 3. Rendering dell'array risolto
return (
<main>
<LayoutRenderer resolvedLayout={resolvedLayout} />
</main>
)
}depth: 99popola a fondo le relazioni (reference, viewMode, styles) richieste dai resolver.UIMS.resolve(block)viene invocato per ogni blocco dellayout, producendo l'arrayresolvedLayout.- Per la SSG,
generateStaticParamselenca gli slug della collectionpages. La home corrisponde alla pagina con slughome.
4.3 LayoutRenderer e BlockRenderer
LayoutRenderer (da /react) è l'entrypoint per un array di blocchi risolti: itera sull'array e delega a BlockRenderer, che gestisce due forme di blocco:
- Standard —
{ Component, slots, ...props }→<Component slots={slots} {...props} />. - Section annidata —
{ section, container, content }→<Section><Container>…content (ricorsivo)…</Container></Section>.
Per un singolo elemento (come un global) è possibile bypassare LayoutRenderer e renderizzare direttamente { Component, ...props }, come nella §4.1.
Riferimento rapido degli import
// Config Payload
import { uimsCollections, uimsGlobals, collectionPages, uimsSections,
uimsFormOverrides, uimsFormSubmissionOverrides } from '@woodylab/payload/config'
// Motore UIMS
import { createUIMSContext, uimsResolvers } from '@woodylab/payload/uims'
// React
import { uimsComponents, LayoutRenderer, UIMSEventProvider, useUIMSEvents, uimsEventHandlers } from '@woodylab/payload/react'
// Utils
import { generateCssVars, getCachedGlobal } from '@woodylab/payload/utils'
// Seeders
import { syncStylesToDB, syncViewModesToDB, seedEvents, viewModeDefaults } from '@woodylab/payload/seeders'
// Manifest (default export)
import uimsManifest from '@woodylab/payload/uimsManifest'