Componenti veri. Tuoi fino all'ultima parte.
Tutto quello che vedi in questa pagina — i bottoni, le schede, le tabelle, la barra qui accanto — è fatto con i componenti che l'agente usa per la tua app. Si usano senza scrivere CSS, si piegano a sette livelli e i blocchi grandi parlano col backend da soli.
| Ordine | Cliente | Stato |
|---|---|---|
| 1042 | Rossi Srl | Pagato |
| 1047 | Bianchi & C. | In attesa |
| 1051 | Verdi Snc | Scaduto |
Una riga, e il componente è finito
Non c'è un foglio di stile da scrivere per avere una schermata che regge: i componenti leggono le variabili CSS del progetto — colori, carattere, raggio — e portano da soli lo stato, il focus, il chiaro e lo scuro. Il tono di un riquadro lo prendono anche i bottoni che ci stanno dentro.
<Alert tone="danger" title="Pagamento rifiutato"
actions={<Button size="sm">Riprova</Button>}>
La carta è stata respinta dalla banca.
</Alert>
<Stat layout="card" label="Ordini" value="1.284"
delta="+12%" trend="up" icon="shopping-bag" />Il bottone «Riprova» non ha un tono scritto: è di pericolo perché sta in un riquadro di pericolo.
Sette livelli, dal valore di una prop a un tuo file
Ci si ferma dove basta. Ogni livello arriva un po' più in là del precedente e costa un po' di più; i primi quattro non toccano il codice di nessun componente e passano da una prop o da client/style.css.
Una prop
variant, tone e size hanno gli stessi valori in tutto il kit: imparati su un componente, valgono per tutti.
<Button variant="soft" tone="success">Salva</Button>
<Button variant="outline" size="lg" icon="plus">Nuovo</Button>
<Badge tone="warning" dot>In attesa</Badge>Le variabili
Il kit legge le variabili standard di shadcn — --primary, --radius, --border e le altre — e nient'altro del tema. Si scrivono in client/style.css: su :root valgono per tutto il progetto, su un contenitore per quello che ha dentro; il chiaro e lo scuro sono due blocchi, :root e .dark.
:root {
--primary: oklch(0.55 0.2 275);
--radius: 0.5rem;
}
.dark {
--primary: oklch(0.7 0.18 275);
}
/* o solo per quello che sta dentro un contenitore */
.vetrina {
--primary: oklch(0.62 0.19 150);
--radius: 1.75rem;
}Il CSS della radice
Ogni componente ha una radice con un indirizzo — [data-slot="card"] — e ogni variante un attributo — [data-variant="soft"]. Il CSS del kit pesa zero e sta in un livello sotto il tuo: una regola scritta da te vince sempre, senza !important.
.bordo [data-slot="card"] {
border-style: dashed;
border-color: color-mix(in oklab, var(--primary) 45%, transparent);
}
.bordo [data-variant="soft"] {
letter-spacing: .02em;
}Il CSS delle parti
Anche ogni parte ha un indirizzo — [data-slot="card-header"], [data-slot="card-title"], [data-state="open"] — e si ridisegna allo stesso modo. Il catalogo elenca le parti di ogni componente.
.sala [data-slot="card-header"] {
background: linear-gradient(100deg,
color-mix(in oklab, var(--primary) 22%, transparent), transparent);
}
.sala [data-slot="card-title"] {
font-family: var(--font-mono);
letter-spacing: .08em;
text-transform: uppercase;
}
.sala [data-slot="badge"] { border-radius: 2px; }Una sola istanza
className sulla radice e classNames sulle parti toccano un componente solo, con le utility che già conosci.
<Card
icon="package"
title="Ordine 1042"
classNames={{ title: "font-heading text-xl italic", icon: "rotate-6" }}
className="border-primary/40"
/>Senza lo stile del kit
unstyled toglie tutto il CSS del kit da quell'istanza e dalle sue parti. Resta il comportamento: l'accessibilità, i gesti, gli stati.
<Button unstyled className="underline decoration-dotted underline-offset-4">
Informativa sulla privacy
</Button>Un tuo file al posto del suo
Un file con lo stesso nome di un componente del kit lo sostituisce — il .tsx e il .css —, e tutti gli altri componenti che lo usano usano il tuo. Per ciò che il kit non prevede, i componenti del progetto vivono nella stessa cartella.
client/_ui/button.tsx il tuo Button, in tutte le superfici
client/_ui/button.css e il suo aspetto
client/app/ui/card.tsx la tua Card, solo nella superficie «app»I blocchi grandi parlano col progetto da soli
Un modulo, una tabella di gestione, l'accesso, la chat con un agente: nella tua app sono una riga, perché sanno già con quale azione, tabella o agente del progetto parlare.
<ActionForm action="ordini.crea" />
Un campo per ogni input dell'azione, col controllo giusto per il suo tipo, i vincoli, gli errori del server messi sotto il campo che li ha causati e il messaggio di esito. Cambi l'azione e il modulo cambia.
// server/ordini/crea.ts dichiara l'input; la pagina scrive una riga
<ActionForm action="ordini.crea" />
// o dispone i campi a mano, senza ridisegnarli
<ActionForm action="ordini.crea">
<Field name="cliente" />
<Field name="note" control="textarea" hint="Le vede solo il magazzino" />
</ActionForm>IVA esclusa
<ResourcePage title="Clienti" list="clienti.list" />
La schermata di gestione intera di una tabella: testata, filtri, elenco, creazione, modifica e cancellazione con il modulo generato dalle azioni, lo stato vuoto e quello d'errore, la paginazione. Colonne, filtri e operazioni si ricavano dalla tabella e da cosa la persona può fare: un permesso che manca toglie il comando invece di spegnerlo. Le date escono nel fuso del progetto, gli importi nella sua valuta, i valori di un elenco con la loro etichetta; con una rotta di dettaglio la riga intera diventa un link. Ogni prop è un modo di dire qualcosa di diverso.
// tutto ricavato dalla tabella clienti
<ResourcePage title="Clienti" list="clienti.list" />
// oppure si dice solo quello che cambia
<ResourcePage
title="Clienti"
list="clienti.list"
columns={[{ key: "nome", label: "Nome", sortable: true, primary: true }, { key: "citta" }]}
create="clienti.apri"
rowHref={(c) => `/clienti/${c.id}`}
/>| Nome | Città | Stato |
|---|---|---|
| Rossi Srl | Torino | Attivo |
| Bianchi & C. | Bari | Sospeso |
| Verdi Snc | Lecce | Attivo |
<LoginForm />
L'accesso del progetto: i metodi che la superficie offre — password, codice via email, account Google — la registrazione, il recupero, il secondo fattore. Quello che cambia fra un progetto e l'altro è nel manifest della superficie, non nella pagina.
// client/app/accedi.tsx
export default definePage(function Accedi() {
return <AuthShell><LoginForm /></AuthShell>;
});<AgentChat agent="assistente" />
La conversazione con un agente del progetto: risposte che arrivano mentre vengono scritte, gli strumenti che l'agente usa mostrati riga per riga, le conversazioni salvate. L'agente lo descrivi come descrivi il resto; la chat c'è già.
// client/app/aiuto.tsx
<AgentChat agent="assistente" placeholder="Chiedi qualcosa sui tuoi ordini" />Qui i blocchi sono disegnati con le primitive del kit: un sito di presentazione non ha un backend a cui collegarli.
Intorno ai blocchi, due piccoli attrezzi dello stack: format.money, format.number, format.date, format.datetime e format.time scrivono i valori nella lingua, nel fuso e nella valuta del progetto; useParams, useNavigate e useSearchParams leggono le rotte della tua app già tipizzate. Una pagina di dettaglio con Loaded mostra il caricamento, l'errore, il «non trovato» e lo stato vuoto senza una riga di controllo.
Altri blocchi che si collegano con una riga
Le parti di un prodotto che tutti riscrivono, già scritte e già collegate.
Account
Dati, password, social collegati, secondi fattori e dispositivi: la pagina delle impostazioni di una persona.
Pay
Un pagamento con carta: porta il cliente alla pagina di pagamento o la apre dentro la tua, i dati della carta non passano mai dalla pagina, e il progetto ne riceve l'esito.
PushToggle
L'iscrizione alle notifiche di chi usa la tua app, sul telefono e nel browser.
ConnectButton
Il collegamento a un servizio esterno — GitHub, il tuo CRM — con il consenso di chi lo concede.
Ogni componente, in ogni variante
Una pagina sola che li mostra tutti nello stato in cui li trova la tua app, con le varianti, i toni e le parti che il catalogo dichiara. È lo stesso kit di questo sito.
Descrivi l'app. I componenti ci sono già.
L'agente li usa per te, e quando vuoi andare più a fondo sai a quale livello fermarti.