Ogni volta che un utente apre un agente, entra in una conversazione. Non in un pannello tecnico, non in un widget anonimo: in un’esperienza che deve sembrare coerente, affidabile, curata — anche quando dietro ci sono layout diversi, temi personalizzati, avatar 3D, drawer di cronologia, pannelli artifact e decine di configurazioni per integrazione.
Per anni abbiamo costruito memori-react così: componente per componente, layout per layout, esigenza per esigenza. Funzionava. Ma con il crescere del prodotto — chat embedded, full page, totem, website assistant, hidden chat — la frammentazione si faceva sentire. Un pulsante qui, una modale lì, un drawer con animazioni diverse altrove. Stessi gesti, aspetti diversi. Stesso brand, comportamenti non sempre allineati.
Non era solo una questione estetica. Era una questione di fiducia. Quando l’interfaccia è incoerente, l’utente percepisce il prodotto come meno solido — e in un contesto conversazionale, dove l’AI deve sembrare presente e attendibile, ogni incoerenza visiva pesa.
A un certo punto la domanda non era più “come sistemiamo questo componente?”, ma “come costruiamo un linguaggio UI che regga su tutto l’ecosistema AIusuru?”. La risposta è stata @memori.ai/ui: una libreria React ad hoc, pensata per la piattaforma AIsuru, con token di design condivisi, componenti accessibili, supporto multilingua e un catalogo documentato in Storybook. Non un kit generico preso dallo scaffale, ma un design system costruito intorno ai pattern che usiamo ogni giorno: chat, form di login, drawer, tooltip, alert, tabelle.
In questo post raccontiamo quel percorso. Prima il perché — cosa non andava e cosa volevamo ottenere. Poi la libreria — architettura, stack e componenti. Infine l’integrazione in memori-react: come abbiamo collegato token, provider, portal e CSS a layer senza perdere la flessibilità che ogni integrazione cliente richiede. E, soprattutto, cosa è cambiato sul campo: un’interfaccia più uniforme, un’esperienza più prevedibile, un widget che finalmente si comporta come un prodotto unico — anche quando ne indossi molti volti diversi.
Perché @memori.ai/ui
Quando abbiamo iniziato a valutare le opzioni, la domanda non era “ci serve una libreria UI?”. Era già chiaro che sì. La domanda vera era: quale, e per chi.
Memori non è un’app con una sola schermata e un solo flusso. È un widget che vive dentro siti di clienti diversi, con colori, temi e layout configurabili. È una piattaforma conversazionale dove convivono chat, avatar, cronologia, upload, artifact, login OTP, feedback, impostazioni. È lo stesso prodotto che deve funzionare su desktop, su mobile, in un totem fisico, in un assistente che si apre da un angolo della pagina.
Le librerie UI generaliste coprono molti casi d’uso — ma raramente i nostri. Un design system pensato per dashboard amministrative non sa cosa fare con una chat bubble che espone azioni contestuali. Ogni volta che forzavamo un componente generico nel nostro contesto, finivamo per scrivere CSS di compensazione, wrapper ad hoc, comportamenti speciali per layout. Il risparmio iniziale si trasformava in debito tecnico.
C’era anche un secondo obiettivo, meno visibile ma altrettanto concreto: uscire dalle librerie che avevamo ereditato. Sulla dashboard AIsuru conviveva Ant Design — un sistema solido, pensato per pannelli amministrativi, non per una conversazione. In memori-react usavamo Headless UI: primitivi accessibili, ma senza identità visiva.
C’era anche un secondo obiettivo, meno visibile ma altrettanto concreto: un’unica libreria al posto di quelle che ogni prodotto aveva ereditato. In memori-react usavamo Headless UI: primitivi accessibili, ma senza identità visiva. Ogni superficie li vestiva a modo suo, e il risultato era prevedibile — stessi gesti, look diversi, CSS di compensazione ovunque. Sulla dashboard AIsuru convive Ant Design: un sistema solido, pensato per pannelli amministrativi, non per una conversazione. Quel ricambio non è ancora iniziato — ma è parte dello stesso disegno: quando la dashboard passerà a @memori.ai/ui, non dovrà inventarsi un altro linguaggio.
Volevamo quindi un linguaggio visivo comune tra la nostra dashboard aisuru.com e memori-react. Se ognuno contiene pulsanti, modali e form in modo indipendente, l’utente finale percepisce due prodotti diversi — anche quando dietro c’è la stessa tecnologia.
La scelta di costruire @memori.ai/ui è nata da lì. Non come esercizio di branding, ma come investimento operativo:
- Controllo sui pattern che contano — drawer, tooltip, alert, form, dropdown, tabelle: componenti che usiamo decine di volte, non una volta all’anno.
- Theming nativo — sistema di colori OKLCH, token CSS semantici, supporto light/dark senza fork del codice per ogni integrazione.
- Accessibilità e i18n dal primo giorno — test automatici, Storybook come documentazione viva, traduzioni integrate per le componenti che ne hanno bisogno.
- Indipendenza di release — libreria pubblicata su npm, versionata e consumabile da memori-react (e da altri progetti) senza legare l’evoluzione UI al ciclo di rilascio di un singolo repository.
In pratica, abbiamo separato due responsabilità che prima erano mescolate: @memori.ai/ui risponde alla domanda “come deve comportarsi un pulsante, una modale, un campo di input nel mondo Memori?”; memori-react risponde a “come si costruisce un’esperienza conversazionale completa attorno a quelle fondamenta?”.
Non è stata la strada più breve. Costruire e mantenere un design system richiede disciplina: convenzioni di naming, review visive, aggiornamenti dei token, compatibilità tra versioni. Ma la strada alternativa — continuare a patchare componenti isolati su centinaia di file — era già più costosa, solo distribuita nel tempo e meno visibile nei planning.
Il risultato che cercavamo era chiaro fin dall’inizio: un widget che sembri progettato tutto insieme, anche se dietro ci sono sei layout, decine di opzioni di configurazione e anni di funzionalità accumulate. @memori.ai/ui è il fondamento su cui quella coerenza poggia. Nel prossimo capitolo vediamo come è fatto — stack, componenti e sistema di design token — prima di entrare nel dettaglio dell’integrazione in memori-react.
Dentro la libreria
@memori.ai/ui non è un insieme di componenti “stilizzati a mano” sparsi in una cartella. È un design system completo: primitivi accessibili, token di design, build pubblicabile, documentazione in Storybook e una API TypeScript pensata per chi integra il widget — non solo per chi lo sviluppa.
Architettura e stack tecnologico
La libreria è costruita su React e TypeScript, con peer dependency su React 17/18 e supporto per i18next / react-i18nextdove i componenti devono parlare la lingua dell’utente.
Per l’interattività e l’accessibilità ci affidiamo a @base-ui/react: primitivi headless che gestiscono focus trap, ARIA, posizionamento e stati senza imporre un look preconfezionato. Su questa base abbiamo costruito l’identità visiva Memori — non il contrario.
Altri mattoni dello stack:
Tecnologia | Ruolo |
|---|---|
TanStack Table | Componente |
lucide-react | Iconografia coerente in tutta la libreria |
OKLCH + | Sistema colore moderno, derivazioni automatiche per hover, focus, bordi |
CSS cascade layers | Separazione netta tra reset, token, componenti e override |
Vitest + axe | Test unitari e controlli accessibilità WCAG in CI |
ESM + CJS | Build dual per consumatori diversi; CSS emesso come file unico |
Il CSS non è un dettaglio secondario: è il contratto di theming. Chi integra la libreria importa @memori.ai/ui/styles.css una volta sola e ottiene reset, variabili, classi componente e comportamento responsive. Il font di default (Lexend Deca) non è bundled — lo carica l’host, o sovrascrive --memori-font-family.
Per esplorare varianti, stati e comportamenti, il riferimento è lo Storybook ufficiale: lì vivono esempi interattivi, controlli delle props e verifiche a11y che nel README restano solo accennate.
I componenti che compongono il sistema
L’API pubblica esporta tutto da @memori.ai/ui. Non serve navigare percorsi interni o importare file sorgente. I componenti sono raggruppati per responsabilità:
Azioni e navigazioneButton (con varianti primary, secondary, outline, ghost, danger, toolbar), Dropdown compound (Trigger, Menu, Item, Separator, Group).
Overlay e contenimentoModal, Drawer, Popover, Tooltip, ConfirmDialog — tutti con gestione del focus e del posizionamento delegata ai primitivi sottostanti.
Form e inputForm, Field (Root, Label, Description, Error, Control), FieldGroup, Input, Checkbox, Slider, Combobox, SelectBox, Autocomplete.
Feedback e statoSpin per loading, sistema alert con AlertProvider, AlertViewport, useAlertManager e createAlertOptions per toast contestuali.
Contenuto e strutturaCard, Tabs (compound), Expandable per testi troncati, Section per header di modulo, Collapsible, Tablecon supporto TanStack.
InfrastrutturaMemoriUIProvider per tema e container dei portal, useTheme per light/dark, MemoriI18nProvider e addMemoriTableToI18n per le traduzioni delle tabelle.
Non è un catalogo infinito — è un set curato di ciò che serve davvero a Memori e AIsuru. Ogni componente nuovo entra nella libreria solo se ha senso in più contesti, non per risolvere un caso isolato.
Design token e sistema di theming
Il cuore visivo della libreria sono i CSS custom properties. Invece di hardcodare colori e spaziature nei componenti, tutto passa da token semantici che l’host può sovrascrivere:
:root {
--memori-primary-color: oklch(0.55 0.22 290);
--memori-secondary-color: oklch(0.7 0.15 200);
--memori-font-family: 'Lexend Deca', sans-serif;
}Da lì derivano automaticamente stati hover, active, focus ring, ombre, bordi. Il dark mode si attiva con data-theme="dark" sul root — nessun fork JavaScript, nessun doppio set di componenti.
Le famiglie di token principali:
Famiglia | Esempi | Uso |
|---|---|---|
Brand |
| Identità visiva per integrazione |
Superfici |
| Sfondi chat, drawer, card |
Tipografia |
| Testo uniforme su tutti i layout |
Spaziatura |
| Ritmo verticale e orizzontale |
Forma |
| Border radius coerenti |
Movimento |
| Animazioni non invasive |
Feedback |
| Alert, validazione, stati |
I componenti espongono classi con prefisso memori- (es. memori-button, memori-drawer, memori-tooltip__popup), così l’host può affinare dettagli senza rompere l’encapsulamento interno.
Questo modello — token → componenti → override mirati — è ciò che rende la libreria flessibile senza diventare un campo minato di !important e hack CSS.
Come l’abbiamo integrata in memori-react
Avere una buona libreria UI è metà del lavoro. L’altra metà è integrarla in un codebase maturo come memori-react — centinaia di componenti, sei layout, configurazioni per cliente, web component, Storybook, test snapshot — senza rompere nulla e senza perdere la personalizzazione che ogni integrazione richiede.
1. Dipendenza e foglio di stile globale
Il primo passo è stato dichiarativo: aggiungere @memori.ai/ui come dipendenza npm e importare il CSS compilato nel foglio di stile globale di memori-react.
Il design system entra una volta sola, all’inizio della catena CSS. Sopra ci si appoggiano gli stili di dominio — chat bubble, layout totem, artifact drawer — che restano responsabilità di memori-react. La libreria fornisce le fondamenta; il widget costruisce l’esperienza conversazionale.
In build, postcss-import inlined l’import prima della pubblicazione, così chi consuma il pacchetto riceve un unico styles.csscoerente.
2. Provider a livello applicazione
Alcuni componenti della libreria richiedono contesto React. In index.tsx abbiamo aggiunto AlertProvider per il sistema di toast globale. In i18n.ts, addMemoriTableToI18n(i18n) fonde le stringhe della libreria con quelle già presenti nel widget — cinque lingue, un’unica istanza i18next.
Non abbiamo duplicato l’infrastruttura di traduzione: l’abbiamo estesa.
3. MemoriUIProvider: tema e portal nel widget
Il punto più delicato dell’integrazione è il widget embedded. Tooltip, dropdown, modali e drawer usano portal per uscire dal DOM locale. Ma in Memori il widget vive dentro un contenitore con altezza fissa, tema proprio e spesso dentro shadow DOM o iframe. Infatti nella nostra dashboard aisuru.com noi importiamo direttamente il widget della chat, il problema era proprio far si che i portal non uscissero dal DOM della chat.
MemoriUIProvider risolve questo: riceve il riferimento al root del widget (container={widgetRootEl}) e il tema (theme={widgetTheme}), e garantisce che tutto ciò che viene portato fuori dal flusso normale del DOM rispetti entrambi.
Accanto, AlertViewport vive dentro il provider tematizzato — così i toast ereditano i token corretti in dark mode.
È un compromesso architetturale nato da vincoli reali: nei layout hidden_chat e website_assistant, il widget collassa a dimensioni minime nel flusso del documento, e i tooltip senza questa gestione finivano per impilare le lettere in verticale. Non è un bug della libreria — è il contesto d’uso che richiede configurazione esplicita.
4. CSS a layer: tema default e override per integrazione
In styles.css abbiamo introdotto cascade layers:
@layer theme, integration;Il layer theme contiene i default del widget (font, colori primari, asse conversazione condiviso). Il layer integration — popolato dinamicamente da integrationConfig — sovrascrive token come --memori-primary-color per il brand del cliente, senza toccare il codice dei componenti.
È il meccanismo che permette a un unico widget di sembrare “del sito che lo ospita” pur restando strutturalmente identico.
5. Migrazione componente per componente
L’integrazione non è stata un big bang. Il commit iniziale (feat: integrate @memori.ai/ui components) ha posizionato le fondamenta; poi decine di commit style: e refactor: hanno migrato un componente alla volta:
- Header e ChatInputs — pulsanti, dropdown consumo crediti, tooltip
- ChatBubble — azioni contestuali, expandable, modali
- ChatHistory — drawer cronologia, select filtri, alert di conferma
- LoginDrawer — form OTP, card, validazione
- ArtifactDrawer — tabs, dropdown azioni, copy/export
- StartPanel — combobox, modal, expandable per hint
- FeedbackButtons, ShareButton, SettingsDrawer, KnownFacts — e molti altri
Oggi oltre sessanta file importano da @memori.ai/ui. I CSS custom su modali, drawer e pulsanti si sono ridotti; quelli che restano gestiscono layout e comportamenti specifici del dominio conversazionale, non la ripetizione di pattern UI già risolti.
Prima e dopo: cosa è cambiato
Coerenza visiva: un solo linguaggio
Prima, aprire il widget in layout diversi significava incontrare micro-differenze ovunque: border radius diversi tra input e bubble, pulsanti con padding non allineato, drawer con animazioni e ombre non uniformi, header che cambiava struttura tra chat e full page.
Dopo l’integrazione, pulsanti, modali, drawer, tooltip e dropdown condividono gli stessi token: stesso raggio, stessa gerarchia tipografica, stessi stati hover/focus/active, stessa logica di spaziatura. L’utente non “vede” @memori.ai/ui — vede un prodotto che non sbaglia i dettagli.
Layout e responsive: l’asse conversazione
Uno dei miglioramenti meno evidenti ma più impattanti è l’introduzione di un asse conversazione condiviso: una larghezza massima (--memori-conversation-max-width), padding inline uniforme e allineamento tra lista messaggi, stato vuoto e area di input.
Prima, ogni layout gestiva questi vincoli in modo indipendente — e su mobile le discrepanze saltavano all’occhio. Dopo, chat, full page e zoomed full body condividono la stessa logica di contenimento, con adattamenti solo dove il layout lo richiede (totem, website assistant).
Anche gli sfondi globali configurabili da integrationConfig (globalBackground) si integrano ora con il sistema di superfici della libreria, senza rompere contrasto o leggibilità in dark mode.
Tema light/dark: finalmente allineato
Il tema non è più una questione di CSS sparsi con override fragili. MemoriUIProvider e data-theme sul root del widget restano sincronizzati: tooltip, alert, drawer e modali portati via portal ereditano gli stessi token del contenitore.
In pratica, passare da light a dark non significa più “sistemare a mano” i componenti che escono dal flusso del DOM — significa affidarsi a un unico punto di verità.
Cosa cambia per l’utente (UX)
La coerenza visiva è il prerequisito; l’esperienza è il risultato. Ecco le aree dove il cambiamento si sente davvero:
Prima | Dopo |
|---|---|
Feedback dopo un’azione (like/dislike, upload, errore) con pattern diversi | Toast uniformi via |
Form di login con validazione e messaggi di errore incoerenti |
|
Menu contestuali implementati in modi diversi per componente |
|
Stati di caricamento disparati (spinner custom, testo “loading…”) |
|
Tooltip che su alcuni layout si comportavano in modo imprevedibile |
|
Cronologia chat difficile da usare su mobile |
|
Testi lunghi nelle risposte senza gestione uniforme |
|
Nessuna di queste righe è “solo UI”. Sono frizioni rimosse da flussi che l’utente attraversa ogni giorno: chiedere qualcosa, leggere una risposta, dare feedback, riprendere una conversazione, accedere, caricare un documento.
In conclusione
Costruire @memori.ai/ui e integrarla in memori-react non è stata una rivoluzione lampo. È stata una migrazione progressiva — commit dopo commit, componente dopo componente — guidata da un obiettivo semplice da enunciare e difficile da raggiungere: far sembrare Memori un prodotto unico, ovunque e in qualunque configurazione.
La lezione che ci portiamo a casa è duplice.
Sul piano tecnico: un design system condiviso paga se è nativo per i tuoi pattern, non adattato a posteriori. Token CSS, provider per portal e tema, wrapper di dominio sottili — questi non sono dettagli di implementazione, sono le fondamenta che permettono di evolvere senza ricominciare da zero a ogni feature.
Sul piano del prodotto: l’utente non distingue tra “componente della libreria” e “componente del widget”. Distingue se l’esperienza è fluida, prevedibile, curata. @memori.ai/ui ci ha dato il linguaggio comune; memori-react continua a raccontare la storia conversazionale. Insieme, per la prima volta, raccontano la stessa storia.
Risorse utili
- Libreria UI: github.com/memori-ai/ui
- Storybook componenti: memori-ai.github.io/ui
- Widget React: github.com/memori-ai/memori-react
- Storybook widget: memori-ai.github.io/memori-react