Vai al contenuto principale

Diario di bordo · Fase 0 · 6/7

Un design system Catppuccin, glass-first

Ventisei token Catppuccin per flavor, un glassmorphism al 62% di trasparenza e tre famiglie tipografiche self-hosted: la pelle di aubia.dev, dal token CSS al reduced-motion.

Pubblicato il 17 agosto 20265 min di lettura
  • design-system
  • catppuccin
  • react

Una volta archiviato il backend in moduli, bisognava dare un volto ad aubia.dev. Volevo un'identità serrata, scura per impostazione predefinita, che respirasse cura senza gridare. Il risultato si riduce a quattro scelte di fondo: una palette Catppuccin, un theming che si imposta da sé, un linguaggio visivo in vetro, e un motion misurato. Il codice reale del repo sostiene ciascuna di esse.

Catppuccin: quattro flavor, due per impostazione predefinita

La palette viene da Catppuccin, una famiglia di colori ben nota nell'ecosistema dev. Aubia ne include quattro flavor, ma la landing ne espone solo due: Mocha in scuro, Latte in chiaro. I ventisei token di ciascun flavor sono dichiarati come variabili CSS nel blocco @theme di resources/css/app.css. Tailwind v4 si configura interamente in CSS qui, senza alcun file tailwind.config.js.

@theme {
    --color-ctp-base: #1e1e2e;
    --color-ctp-mantle: #181825;
    --color-ctp-text: #cdd6f4;
    --color-ctp-mauve: #cba6f7;
    /* ... vingt-six tokens pour Mocha */
}

Mauve è il mio accento primario. Gli altri due flavor, Frappé e Macchiato, sono inclusi come varianti scure per la Fase 1, ma restano fuori dallo switcher pubblico finché la landing mantiene la sua identità serrata. Il punto chiave: nessun componente fa riferimento a un valore esadecimale hardcoded. Una classe come bg-ctp-mauve cambia valore con il flavor attivo, senza che il JSX ne sappia nulla.

Il theming si imposta da sé

La scelta del tema oggi segue una regola semplice: scuro per impostazione predefinita. Ad aprile, la landing partiva dall'opposto, prima il sistema, leggendo prefers-color-scheme al montaggio per applicare Mocha o Latte secondo l'impostazione del sistema operativo. Ho ribaltato quel default in scuro a giugno. Ora un visitatore arriva in Mocha in ogni caso, e segue l'impostazione del suo sistema operativo solo se sceglie esplicitamente l'opzione «sistema» nello switcher. Il flavor attivo è impostato su <html data-theme="...">, e i valori di Latte scavalcano quelli di Mocha attraverso quell'attributo. Ogni scelta esplicita, chiaro, scuro o sistema, è persistita in localStorage e ha la precedenza sul default al ritorno successivo.

Questo cambiamento non è cosmetico. Il bersaglio di Aubia sono sviluppatori che vivono in modalità scura: meglio accoglierli nel loro ambiente naturale che infliggere loro un lampo bianco all'apertura della pagina.

Glass-first: la trasparenza come presa di posizione

Il linguaggio visivo della landing poggia su card in glassmorphism: sfondo semi-trasparente, bordo sottile, sfocatura di sfondo. È ciò che lascia intravedere il gradiente della pagina attraverso ogni superficie e dà quel senso ovattato di profondità. Piuttosto che ripetere la stessa pila di classi su ogni card, la ricetta vive in un'utility di Tailwind v4 dichiarata in CSS:

@utility glass-card {
    background: color-mix(in oklch, var(--glass-bg-card) 62%, transparent);
    backdrop-filter: blur(40px) saturate(1.6) brightness(1.02);
    border: 1px solid color-mix(in oklch, var(--glass-stroke) 12%, transparent);
    box-shadow: inset 0 1px 0 color-mix(in oklch, var(--glass-highlight) 8%, transparent);
}

Il dettaglio che fa o disfa l'effetto è la trasparenza. Uno sfondo opaco annulla il backdrop-filter: senza il 62% del color-mix, la sfocatura non ha più nulla da sfocare e la card diventa un semplice rettangolo. Il saturate e il brightness restituiscono al vetro un po' di vita che la sola sfocatura spegnerebbe, e l'ombra è interna, un semplice filo di luce sul bordo superiore. La disciplina è rigorosa: sfondo traslucido, bordo appena marcato, mai un'ombra portata pesante che romperebbe l'illusione.

Tre famiglie tipografiche self-hosted

La tipografia poggia su tre famiglie, tutte ospitate localmente: Geist per il corpo e l'interfaccia, Geist Mono per il codice e i percorsi di file, Chakra Petch per i grandi titoli display.

Nessuna chiamata di rete a una foundry di terze parti: le dichiarazioni @font-face sono generate in fase di build dal plugin di font di Laravel, a partire dai pacchetti installati. A giugno, ho spostato il caricamento sulla direttiva @fonts, che inietta queste dichiarazioni nell'<head> prima del foglio di stile principale.

Ospitare localmente va oltre la performance: chiamare una foundry di terze parti farebbe trapelare l'IP di ogni visitatore verso un subprocessore fuori dall'UE, cosa che rifiuto per un sito che si vuole irreprensibile sulla privacy. Ho composto questo trio di proposito. Il wordmark di Aubia esige Chakra Petch, e mantenere un Inter per il corpo avrebbe creato una divergenza permanente con quel titolo. Geist e Geist Mono vengono dalla stessa famiglia, il loro allineamento verticale tra prosa e codice è nativo.

Il blocco @theme resta la fonte di verità per i tre font stack:

--font-display: 'Chakra Petch', 'Chakra Petch fallback', system-ui, sans-serif;
--font-sans: 'Geist', 'Geist fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Mono fallback', ui-monospace, monospace;

Il font di fallback che segue ogni famiglia neutralizza lo spostamento del layout al momento dello swap. Solo Chakra Petch è precaricato, perché porta il titolo principale e pesa sul rendering percepito.

Il motion, con un fermo di sicurezza

Le animazioni passano da Motion. Ho centralizzato le varianti riutilizzabili in un unico file, motion-presets.ts, invece di ridefinirle in ogni componente. La comparsa standard delle sezioni è un fade in risalita, con l'easing firma Luma:

export const fadeUpSpring: Variants = {
    hidden: { opacity: 0, y: 24 },
    visible: {
        opacity: 1,
        y: 0,
        transition: { duration: 0.6, ease: EASE_LUMA },
    },
};

Il fermo di sicurezza è l'accessibilità. Un visitatore che ha chiesto meno movimento a livello di sistema non deve subire i fade. Motion rispetta prefers-reduced-motion nativamente, e io normalizzo il suo valore con un hook useSafeReducedMotion() che restituisce sempre un booleano, anche durante il rendering lato server dove la media query non è ancora valutata. Il budget di animazione è deliberatamente stretto: raramente più di due elementi animati alla volta nel campo visibile, per gerarchizzare l'attenzione invece di disperderla.

Queste scelte visive servono una convinzione di prodotto che dettaglio nella sezione Vision della landing. L'ultimo articolo della serie guarda a ciò che i motori stessi vedono di tutto questo: il SEO multilingue e l'estrazione da parte dei motori di risposta IA. Se lo spirito di Aubia le parla, può iscriversi alla lista d'attesa.

Questo cantiere si racconta qui, articolo dopo articolo. Salite a bordo: i vostri riscontri disegneranno il seguito.

Iscriviti alla lista d'attesa