1fc0650dcf
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
260 lines
17 KiB
Markdown
260 lines
17 KiB
Markdown
# 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-6` in tables, `p-4` in
|
||
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-mono` and 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/google`
|
||
as `--font-plus-jakarta-sans`, mapped to Tailwind's `--font-sans` in
|
||
`@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`
|
||
|
||
## 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 `@theme` as `--radius-lg` (`=
|
||
--radius`) and `--radius-xl` (`= --radius + 0.25rem`), so `rounded-lg` /
|
||
`rounded-xl` utilities resolve consistently off one token.
|
||
- `--shadow-card: 0 4px 20px rgba(0,0,0,0.01)` → `shadow-card` utility.
|
||
Default resting elevation for cards, table containers, kanban cards.
|
||
- `--shadow-card-hover: 0 8px 30px rgba(0,0,0,0.02)` → `shadow-card-hover`
|
||
utility. 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`, subtitle `text-xs text-muted-foreground mt-1`, primary
|
||
action button `bg-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 use
|
||
`whitespace-nowrap transition-opacity duration-200` and fade to
|
||
`opacity-0` immediately 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-200` on rows,
|
||
buttons, nav links.
|
||
- **Kanban drag**: dragged card renders in a `DragOverlay` with
|
||
`rotate-1 shadow-xl`; drop target column highlights via `border-primary
|
||
bg-primary/5`.
|
||
|
||
## UX Rules
|
||
|
||
1. Numeric/tabular data is always monospace and right-aligned in tables.
|
||
2. Every interactive control has a visible focus state (`focus:ring-1
|
||
focus:ring-ring focus:border-primary` pattern) for keyboard accessibility.
|
||
3. Status is always communicated redundantly — color + text label — never
|
||
color alone.
|
||
4. Destructive actions use `destructive` tokens, never ad-hoc red.
|
||
5. Empty states are muted, centered, and brief ("Nessun lead trovato").
|
||
6. 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-card` metric cards (Clienti Attivi · Contrattualizzato ·
|
||
Incassato Reale · Ore Tracciate). Value `text-2xl font-bold font-mono`, label
|
||
`text-[10px] uppercase text-muted-foreground`. Positive delta uses
|
||
`text-emerald-600 dark:text-emerald-400`.
|
||
- **Griglia `lg:grid-cols-3`**: 2/3 col = `ForecastChart` (istogramma flussi 12
|
||
mesi, mese prossimo evidenziato `bg-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_at` confrontato con l'ultimo
|
||
messaggio autore `client`. Una conversazione è non letta se esiste un messaggio
|
||
cliente dopo `admin_last_read_at` (o se è `NULL`). Query in
|
||
`src/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 da
|
||
`getUnreadConversationsCount()` in `src/app/admin/layout.tsx`. Stessa fonte di
|
||
verità del `MessagesWidget` in dashboard.
|
||
- **Reply**: salvata come commento `general` sul cliente e aggiorna
|
||
`admin_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 monetari `font-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-60` con 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"
|
||
(`Button` primario) che espande un form inline (`Input` nome + "Crea").
|
||
- **Filtri**: pill categoria rettangolari (`rounded-lg px-4 py-2`) — attiva
|
||
`bg-primary text-primary-foreground`, inattiva `border-border bg-card
|
||
text-muted-foreground hover:bg-muted`. "Tutti" + `categoryOptions` dinamiche.
|
||
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 sono `opacity-60` con pill "Archiviata".
|
||
- **Tabella tier**: `border-border rounded-lg`, header uppercase muted
|
||
(Tier · Servizi · Pubblico), righe `font-mono` `divide-y divide-border`; solo i
|
||
tier con `servicesTotal > 0` o `public_price` valorizzato; "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-card` sticky 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 a `dark:` 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 follow `rounded-xl border-border-light
|
||
bg-card shadow-card`.
|
||
- `ClientDashboard` takes an `embedded` prop: 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.
|