Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
Design System: iamcavalli Admin & Client Portal (v1.0)
"Minimalist Premium / Quiet Luxury" — the aesthetic that replaces the old mixed token/hardcoded styling across ClientHub's admin area. First applied to the Lead Pipeline page (Phase: Pipeline redesign); the tokens and primitives documented here are the base for restyling every other section.
Philosophy
- Quiet, not loud. Elevation is nearly invisible (shadow opacity ≤ 0.02), borders are hairline, color is used sparingly and only to carry meaning (status, semantics) — never for decoration.
- Density with air. Generous padding (
py-4 px-6in tables,p-4in kanban columns) paired with small type (text-xs,text-[11px]) reads as premium rather than cramped. - Numbers are monospace. Prices, phone numbers, dates, counts — anything
tabular/numeric — use
font-monoand right-alignment so columns of digits line up. - Dual-theme by construction. Every surface is built from semantic
tokens (
bg-card,text-muted-foreground,border-border, …), never raw Tailwind palette classes (bg-white,text-slate-900, …) or hex literals. This is what makes dark mode "just work" without a parallel dark stylesheet. - Motion is functional. Transitions exist to explain state change (sidebar
collapse, hover, drag) —
duration-200–duration-350,ease-[cubic-bezier(0.4,0,0.2,1)]for the sidebar specifically. No decorative animation.
Typography
- Sans: Plus Jakarta Sans (weights 300–700), loaded via
next/font/googleas--font-plus-jakarta-sans, mapped to Tailwind's--font-sansin@theme. Used for all UI text. - Mono: Geist Mono,
--font-geist-mono→--font-mono. Used for numeric data cells only (prices, phone numbers, counts, dates in tables). - Scale conventions:
- Page title:
text-2xl font-semibold tracking-tight - Page subtitle:
text-xs text-muted-foreground - Table header cells:
text-[11px] font-semibold uppercase tracking-wider text-muted-foreground - Table body:
text-sm - Badge/pill label:
text-[10px]–text-xs font-semibold uppercase tracking-wide
- Page title:
Color Tokens
Raw values live in :root (light) / .dark (dark) in src/app/globals.css
and are mapped to Tailwind utilities via @theme inline. Components must
consume the mapped utility, never the raw hex or a Tailwind palette shade.
| Utility | Light | Dark | Usage |
|---|---|---|---|
bg-background / text-foreground |
#ffffff / #1a1a1a |
#0e1512 / #f2f4f3 |
Page canvas |
bg-card / text-card-foreground |
#ffffff |
#131a16 |
Cards, table, kanban cards, header bar |
bg-muted / text-muted-foreground |
#f9f9f9 / #71717a |
#171f1b / #9aa39e |
Subtle backgrounds, secondary text, kanban columns |
bg-primary / text-primary-foreground |
#1A463C |
#3FA88C |
Primary actions, sidebar, active nav state |
bg-accent |
#DEF168 |
#DEF168 |
Rare highlight accent (lime) |
border-border / border-input |
#e5e7eb |
#26302b |
All hairline borders |
ring-ring |
#1A463C |
#3FA88C |
Focus rings |
bg-destructive |
#ef4444 |
#f87171 |
Destructive actions |
Sidebar exception: the sidebar stays brand green bg-[#1A463C] in both
themes (it is not a themed surface — it's the constant brand anchor).
Status/semantic colors (lead stages, badges) use Tailwind's default
palette (blue/purple/amber/orange/emerald/red) directly, each paired with an
explicit dark: variant for legibility on dark surfaces — see StatusBadge.
Elevation & Radius
--radius: 0.5rem(base) is wired into@themeas--radius-lg(= --radius) and--radius-xl(= --radius + 0.25rem), sorounded-lg/rounded-xlutilities resolve consistently off one token.--shadow-card: 0 4px 20px rgba(0,0,0,0.01)→shadow-cardutility. Default resting elevation for cards, table containers, kanban cards.--shadow-card-hover: 0 8px 30px rgba(0,0,0,0.02)→shadow-card-hoverutility. Applied on hover for interactive cards.
Layout Conventions
- Table container:
bg-card rounded-xl border border-border shadow-card overflow-hidden. - Table header row:
bg-muted/50 border-b border-border text-[11px] uppercase tracking-wider text-muted-foreground. - Table body rows:
py-4 px-6,divide-y divide-border,hover:bg-muted/40 transition-colors. - Kanban column:
bg-muted/60 rounded-xl border border-border p-4. - Kanban card:
bg-card rounded-lg border border-border shadow-sm hover:border-primary/30 hover:shadow-card-hover transition-all. - Page header: title
text-2xl font-semibold tracking-tight text-foreground, subtitletext-xs text-muted-foreground mt-1, primary action buttonbg-primary text-primary-foreground hover:bg-primary/90.
Color/Class Migration Map (dual-theme principle)
When restyling any surface, replace hardcoded classes with token utilities:
| Old (hardcoded) | New (token) |
|---|---|
bg-white |
bg-card |
bg-slate-50 / bg-gray-50 |
bg-muted |
border-slate-100 / border-slate-200 |
border-border |
text-slate-900 / text-[#1a1a1a] |
text-foreground |
text-slate-500 / text-slate-400 / text-[#71717a] |
text-muted-foreground |
bg-brand-dark / text-[#1A463C] (as action color) |
bg-primary / text-primary |
Motion
- Sidebar collapse:
transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]on the<aside>; label/logo text usewhitespace-nowrap transition-opacity duration-200and fade toopacity-0immediately on collapse, but only fade back in ~150ms after the width transition starts on expand (avoids text reflow/wrap during the animation). - Hover states:
transition-colors duration-150–duration-200on rows, buttons, nav links. - Kanban drag: dragged card renders in a
DragOverlaywithrotate-1 shadow-xl; drop target column highlights viaborder-primary bg-primary/5.
UX Rules
- Numeric/tabular data is always monospace and right-aligned in tables.
- Every interactive control has a visible focus state (
focus:ring-1 focus:ring-ring focus:border-primarypattern) for keyboard accessibility. - Status is always communicated redundantly — color + text label — never color alone.
- Destructive actions use
destructivetokens, never ad-hoc red. - Empty states are muted, centered, and brief ("Nessun lead trovato").
- Sidebar collapse state persists across page loads via
localStorage(iamcavalli:sidebar) and is restored before paint where possible to avoid layout jump.
Component Inventory
Primitives extracted from the Lead Pipeline redesign, intended for reuse across all future admin pages.
| Component | Path | Purpose |
|---|---|---|
AdminShell |
src/components/admin/AdminShell.tsx |
Client shell holding sidebar-collapsed state (persisted to localStorage), renders sidebar + top header + <main>. Wraps all /admin/* pages. |
AdminSidebar |
src/components/admin/AdminSidebar.tsx |
Collapsible brand sidebar (w-64 ↔ w-20), controlled by AdminShell. Nav items, theme toggle, sign-out. |
StatusBadge |
src/components/ui/StatusBadge.tsx |
Rounded-full status pill with a color map per lead stage, each with a dark: variant. Replaces the old scattered STAGE_COLOR maps. |
SearchInput |
src/components/ui/SearchInput.tsx |
Search-icon input, rounded-lg border-border focus:ring-primary. |
SegmentedToggle |
src/components/ui/SegmentedToggle.tsx |
Generic Lista/Kanban-style segmented control (bg-muted p-1 rounded-lg, active option bg-card shadow-sm). |
PageHeader |
src/components/admin/PageHeader.tsx |
Page title (text-2xl tracking-tight) + subtitle + action slot, token-based. |
LeadTable |
src/components/admin/leads/LeadTable.tsx |
Luxury table restyle: bg-card rounded-xl shadow-card, uppercase muted headers, hover:bg-muted/40 rows, uses StatusBadge. |
LeadsKanbanBoard |
src/components/admin/leads/LeadsKanbanBoard.tsx |
Kanban restyle: bg-muted/60 columns with count pills, bg-card cards with hover:border-primary/30. Uses @dnd-kit, keeps all 6 lead stages. |
LeadsViewToggle |
src/components/admin/leads/LeadsViewToggle.tsx |
Wires SearchInput + SegmentedToggle together with the table/kanban views. |
MilestoneStepper |
src/components/client/MilestoneStepper.tsx |
Client-portal horizontal milestone stepper: one node per real phase, connecting line filled by global_progress_pct. Done = filled bg-primary ✓, active = border-primary outline, upcoming = bg-muted. |
LeadDetail |
src/components/admin/leads/LeadDetail.tsx |
Lead detail page (/admin/pipeline/[id]): bespoke header (name + inline StatusBadge + action bar), asymmetric lg:grid-cols-3 layout (1/3 profile card w/ icon-chip rows + tags, 2/3 Note/Follow-up + unified timeline). |
ClientProfitability |
src/components/admin/dashboard/ClientProfitability.tsx |
Dashboard widget: valore orario reale per cliente (getClientProfitability → contrattualizzato ÷ ore tracciate) con badge margine (Ottimo bg-primary/10 text-primary / In Linea bg-muted). |
YearSelector |
src/components/admin/YearSelector.tsx |
Selettore anno restilizzato a pill (rounded-full border-border bg-card shadow-card), frecce ←/→, valore font-mono. Guida i dati year-scoped della dashboard via ?year=. |
ConversationsView |
src/components/admin/conversazioni/ConversationsView.tsx |
Inbox messaggi clienti (/admin/conversazioni) a due pannelli: lista conversazioni filtrabile (search su nome/brand/ultimo messaggio, pallino unread) + thread attivo con bolle messaggio (admin a destra bg-primary, cliente a sinistra), badge entità per messaggio (Generale/Fase/Task/Deliverable) e form di risposta. Auto-mark-read all'apertura. |
MessagesWidget |
src/components/admin/dashboard/MessagesWidget.tsx |
Widget dashboard "Messaggi Clienti": chat clienti in attesa di risposta (getConversations() filtrato su unread), pill emerald "N Nuovi" + pallino pulsante, righe con anteprima e link Rispondi → deep-link a /admin/conversazioni?c=<clientId>. Empty state "Nessun messaggio in attesa ✓". |
CopyLinkButton |
src/components/admin/CopyLinkButton.tsx |
Icon button che copia un URL assoluto (window.location.origin + path) negli appunti; swap icona → check emerald per 1.5s con tooltip "Copiato!". Usato nella lista Clienti per il link profilo pubblico. |
ClientRow |
src/components/admin/ClientRow.tsx |
Riga tabella Clienti restilizzata Quiet Luxury: nome link → dettaglio + sottotitolo brand/progetti, colonne numeriche font-mono right-aligned (LTV, incassato come pill emerald, da saldare), cella "Link profilo" con pill mono troncata (apre portale in new tab) + CopyLinkButton. |
Unified timeline pattern: activities + transcripts merged into one
reverse-chronological "Storico delle Interazioni". Vertical connector = absolute
w-px bg-border at left-[19px] behind w-10 h-10 rounded-full nodes.
Transcript node is highlighted (bg-primary text-primary-foreground) and
expandable (line-clamp-2 → "Mostra tutto/meno"); activity nodes are muted
(bg-muted border-border). Dates are font-mono — activities relative
(formatDistanceToNow), transcripts absolute (d MMMM yyyy).
Admin Dashboard restyle notes
The /admin dashboard was migrated to "Quiet Luxury" (applied 2026-07-11),
replicating design-reference/pagina-dashboard. It is a condensed single-screen
layout: no in-<main> PageHeader (sidebar + shell header carry the chrome).
- KPI strip: 4
bg-cardmetric cards (Clienti Attivi · Contrattualizzato · Incassato Reale · Ore Tracciate). Valuetext-2xl font-bold font-mono, labeltext-[10px] uppercase text-muted-foreground. Positive delta usestext-emerald-600 dark:text-emerald-400. - Griglia
lg:grid-cols-3: 2/3 col =ForecastChart(istogramma flussi 12 mesi, mese prossimo evidenziatobg-primary) +ClientProfitability; 1/3 col =FollowUpWidget(count pill amber) +MessagesWidget("Messaggi Clienti", pill emerald "N Nuovi" + pallino pulsante, chat clienti in attesa di risposta) +OffersSoldChart("Offerte Più Richieste"). - Year selector interattivo (
YearSelector) in una toolbar top-right; governa i KPI year-scoped + redditività. Il forecast è rolling-12-mesi (non year-scoped). - Widget rimossi dal legacy: chart incassi mensili, card "Da incassare"/"Clienti acquisiti", barre "Ore per cliente" (sostituite dalla redditività oraria).
Conversazioni (inbox) notes
/admin/conversazioni è l'inbox unificata dei messaggi clienti (replica
design-reference/pagina-conversazioni). Ogni conversazione è per cliente:
raccoglie tutti i suoi commenti — general + quelli su fasi/task/deliverable —
in un unico thread ordinato, con badge entità per messaggio.
- Fonte unread:
clients.admin_last_read_atconfrontato con l'ultimo messaggio autoreclient. Una conversazione è non letta se esiste un messaggio cliente dopoadmin_last_read_at(o se èNULL). Query insrc/lib/conversations-queries.ts(getConversations,getConversationThread,getUnreadConversationsCount). - Badge sidebar: voce "Conversazioni" (
AdminSidebar) con pill emerald del numero di clienti con non letti (pallino verde se collapsed), alimentato dagetUnreadConversationsCount()insrc/app/admin/layout.tsx. Stessa fonte di verità delMessagesWidgetin dashboard. - Reply: salvata come commento
generalsul cliente e aggiornaadmin_last_read_at(mark-read). L'apertura di una conversazione fa auto-mark-read. Deep-link a un thread specifico via?c=<clientId>.
Clienti (lista) notes
/admin/clients migrata a "Quiet Luxury" (replica design-reference/pagina-clienti).
Tabella bg-card rounded-xl border-border shadow-card con header uppercase muted e
righe py-4 px-6 / divide-y divide-border / hover:bg-muted/40.
- Colonne: Cliente (nome link →
/admin/clients/[id]+ sottotitolo brand/progetti) a sinistra; LTV, Importo incassato, Importo da saldare, Link profilo tutte right-aligned. Valori monetarifont-mono; "incassato" reso come pill emerald (bg-emerald-50 text-emerald-700+ variante dark), zero →—muted. - Link profilo: pill mono troncata (
/client/<slug|token>…) che apre il portale in new tab +CopyLinkButton(copia URL assoluto). - Header actions: link testuale "Mostra/Nascondi archiviati" (
?archived=1) + Button primario "Nuovo cliente". Footer tabella "Mostrando N clienti attivi" (singolare/plurale gestiti). - Le righe archiviate restano
opacity-60con pill "Archiviato".
Offerte (lista) notes
/admin/offers migrata a "Quiet Luxury" (replica design-reference/pagina-offerte).
Griglia responsive di card bg-card rounded-xl border-border p-6 shadow-card
(hover:shadow-card-hover), md:grid-cols-2 lg:grid-cols-3.
- Header:
PageHeader"Offerte" + sottotitolo, azione "+ Nuova Offerta" (Buttonprimario) che espande un form inline (Inputnome + "Crea"). - Filtri: pill categoria rettangolari (
rounded-lg px-4 py-2) — attivabg-primary text-primary-foreground, inattivaborder-border bg-card text-muted-foreground hover:bg-muted. "Tutti" +categoryOptionsdinamiche. A destra checkbox "Mostra offerte archiviate". - Card: nome interno + descrizione muted + badge categoria colorato per tipo
(Retainer→blue, Entry→purple, Signature→amber, tutti con variante dark) via
helper
categoryBadgeClass. Le archiviate sonoopacity-60con pill "Archiviata". - Tabella tier:
border-border rounded-lg, header uppercase muted (Tier · Servizi · Pubblico), righefont-monodivide-y divide-border; solo i tier conservicesTotal > 0opublic_pricevalorizzato; "Pubblico" assente →—.
Client Portal restyle notes
The /client/[token] portal was migrated to the same "Quiet Luxury" tokens
(applied 2026-07-10). Key conventions specific to the client-facing surface:
- Header:
bg-cardsticky bar, three zones —iamcavalli | Client Portal(left), brand name centered, "Area Riservata Protetta" emerald pill (right). - Status pills (phases, payments) use soft tints, not solid fills:
emerald-50/700 (done/saldato), amber-50/700 (in corso/da saldare),
bg-muted/text-muted-foreground(upcoming) — each with adark:variant. - Phase cards (
PhaseCard):rounded-xl border-border-light bg-card shadow-card, progress bar colored per status (emerald/amber/bg-border). - Sidebar cards (
OffersSection,PaymentStatus,DocumentsSection,NotesSection,TranscriptsSection) all followrounded-xl border-border-light bg-card shadow-card. ClientDashboardtakes anembeddedprop: when rendered inside the multi-project tabs wrapper (page.tsx), it drops its own header/footer/canvas so the page-level chrome isn't duplicated.