STATUS.md: perché la pill sta su "Cancellata" e non su "Fatto" (sono entrambi barrati, e scambiarli significa credere consegnato ciò che non esiste), perché `due_date` è nullable, e le due verifiche che restano a mano — il riquadro "Prossimo pagamento" non compare finché nessuna rata ha una scadenza, e due `paid_at` storici valgono il primo del mese perché li ha scritti il vecchio selettore a mese. DESIGN-SYSTEM.md: la quinta forma, la regola della colonna Kanban che si nasconde solo da vuota, e la spec del box pagamenti. STATE.md: due decisioni nuove, e resta sotto le 100 righe accorpando le righe di tabella della stessa settimana. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
30 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. Layout compatto (py-3, due sole righe di testo: Step N + stato in linea, titolo fase sotto) — mock: design-reference/Client-Portal-Progress-Bar. |
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 tab per canale (Generale + una per fase, pallino accent sul tab non letto) e bolle messaggio (admin a destra bg-primary, cliente a sinistra). Badge entità solo sui messaggi storici di task/deliverable. La risposta va sull'entità del canale attivo. Auto-mark-read all'apertura, su snapshot di adminLastReadAt. |
ChatPanel |
src/components/client/ChatPanel.tsx |
Chat del portale cliente: FAB h-14 w-14 fisso in basso a destra con pallino unread, drawer laterale fixed inset-y-0 right-0 bg-card shadow-2xl che in expanded diventa full-screen (Maximize2/Minimize2, Esc chiude prima l'espansione poi il pannello). Tab canale scrollabili orizzontalmente (no-scrollbar), invio e modifica ottimistici, polling GET /api/client/chat ogni 20s. La firma di chi risponde arriva da settings via ChatData, non è più cablata. |
MessageRow |
src/components/client/ChatPanel.tsx |
Una riga del feed, componente a sé perché la modifica ha bisogno di stato per messaggio (bozza, salvataggio, errore). Divider di giornata, raggruppamento dei messaggi consecutivi dello stesso autore entro GROUP_WINDOW_MS, badge entità per lo storico di task/deliverable, «(modificato)» inline dopo il corpo. |
Avatar (chat) |
src/components/client/ChatPanel.tsx |
Cerchio h-9 w-9 con immagine da URL esterno e fallback al monogramma se src manca o il caricamento fallisce (onError). <img> nudo e non next/image: l'URL lo incolla l'utente in Impostazioni e non è noto in fase di build, quindi non si può dichiarare l'host in next.config. |
ChatProvider |
src/components/client/ChatProvider.tsx |
Context del solo stato UI (aperto/espanso/canale attivo) — i messaggi restano dentro ChatPanel, unico a fare polling e a tenere la copia optimistic. Il canale sta nel context perché lo decide anche chi è fuori dal pannello: openChat(phaseId) dalla bolla di una PhaseCard apre la chat già sulla fase giusta. |
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.html. 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).
Chat a canali (portale + inbox) notes
I messaggi vivono in canali: "Generale" più uno per fase. Il canale non è una
colonna — comments resta polimorfica (entity_type + entity_id) — ma una
chiave derivata, e la derivazione sta in un posto solo:
src/lib/chat-channels.ts, funzioni pure senza accesso al DB, importate da
entrambe le sponde.
"Generale" -> clients.id
una fase -> phases.id
task -> la chiave della fase proprietaria (tasks.phase_id)
deliverable -> la chiave della fase del task proprietario
Perché in un modulo condiviso: se cliente e admin calcolassero il canale ognuno per conto suo, una risposta atterrerebbe in un tab diverso da quello in cui è stata scritta la domanda. Task e deliverable non sono più scrivibili da nessuna UI, ma lo storico esiste e rientra nel canale della fase, conservando il nome dell'entità come badge sul messaggio.
Il letto/non-letto è asimmetrico fra le due sponde, di proposito:
- Cliente → tabella
client_channel_reads(migration 0021), una riga per(client_id, channel_key)con upsertON CONFLICT … DO UPDATE. Serve una riga per canale perché un singoloclient_last_read_atsegnerebbe letti tutti i canali all'apertura della chat, e il pallino sul singolo tab — l'unica cosa che dice al cliente dove guardare — perderebbe senso. - Admin → resta su
clients.admin_last_read_at(0014): lì la lettura è già a livello di conversazione, e il pallino per canale si deriva da quel timestamp. Nessuna tabella nuova.
/admin/conversazioni (replica design-reference/pagina-conversazioni.html)
resta a due pannelli, con i canali come tab dentro il thread attivo.
- 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). Il thread porta con séadminLastReadAtcome snapshot: congelato al caricamento, così l'auto-mark-read all'apertura non cancella i pallini per canale sotto gli occhi di chi sta leggendo. - 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: scritta sull'entità del canale attivo (cliente per "Generale",
fase per gli altri) — non più sempre
general— e aggiornaadmin_last_read_at. Deep-link a un thread via?c=<clientId>.
Indice: comments non ha mai avuto un indice, nemmeno su entity_id, dal
giorno 0. La 0021 aggiunge (entity_id, created_at), che è esattamente il modo
in cui il feed si legge.
Pattern «Modifica» sui messaggi (portale e inbox admin, stessa forma): pulsante
testuale piccolo, non un'icona, coerente con i già presenti Riprova/Elimina. Si
rivela al passaggio del mouse ma deve restare raggiungibile senza puntatore:
opacity-0 transition-opacity group-hover:opacity-100
focus-visible:opacity-100 [@media(hover:none)]:opacity-100
L'ultima clausola non è un dettaglio — senza, il pulsante è irraggiungibile da telefono,
ed è da telefono che il cliente apre il portale. In modifica la bolla diventa una
textarea: Invio salva, Esc annulla, più i due pulsanti espliciti. Nell'inbox admin i
controlli stanno fuori dalla bolla: sopra bg-primary un testo tenue non si legge.
Il pulsante nascosto non è una difesa — chi può modificare lo ridecide il server.
Clienti (lista) notes
/admin/clients migrata a "Quiet Luxury" (replica design-reference/pagina-clienti.html).
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.html).
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 →—.
Catalogo (griglia servizi) notes
/admin/catalog migrata a "Quiet Luxury" (replica design-reference/pagina-catalogo.html).
È una griglia editabile in stile foglio di calcolo: ServiceTable in un contenitore
bg-card rounded-xl border-border shadow-card overflow-hidden, header uppercase muted
(text-[11px] font-semibold uppercase tracking-wider text-muted-foreground, bg-muted/50),
righe hover:bg-muted/40 / divide via border-border.
- Celle inline: nome/descrizione/prezzo/stato usano
EditableCell; fase usaOptionSelect; offerta/pacchetto usanoOptionMultiSelect. I badge fase/offerta/ pacchetto sono colorati deterministicamente viagetOptionColor(hash → palette a 7 colori) — nessun colore semantico fisso, coerente col sistema tag del resto dell'app. - Colonne: riordinabili via drag (
GripVerticalon hover) e ordinabili sulle colonnesortable(ArrowUp/Downintext-primary); ordine persistito inlocalStorage(catalog_col_order). - Quick-add: ultima riga
bg-muted/40con input inline (+ Aggiungi servizio, prezzo, fase datalist) — Invio ↵ crea il servizio. Servizi disattivati raggruppati sotto un headerbg-muted/40e resiopacity-50. - Search:
SearchInputprimitivo (non più input+icona inline) su nome/categoria/fase/ tag/pacchetto. Footer tabella "Visualizzazione di N servizi" (singolare/plurale). - Nota primitivi: in questa pass
OptionSelectedEditableCell(accent-primary) sono stati tokenizzati — erano rimasti con hex hardcoded mentreOptionMultiSelectera già token-based. Il fix vale per tutte le pagine che li usano (Offerte, Lead).
Preventivi (lista) notes
/admin/preventivi migrata a "Quiet Luxury" (replica design-reference/pagina-preventivi.html).
Tabella in contenitore bg-card rounded-xl border-border shadow-card overflow-hidden, header
uppercase muted (bg-muted/50, text-[11px] font-semibold uppercase tracking-wider), righe
hover:bg-muted/40 con divide-y divide-border.
- Header:
PageHeader"Preventivi" + sottotitolo, azioneButton"Genera preventivo" (+, link a/admin/preventivi/genera). - Colonne: Titolo (link → dettaglio,
font-semibold hover:text-primary), Lead/Cliente, Offerta (muted), Stato (badge centrato), Data (font-mono text-xs), Azione. - Badge stato: pill rounded-full
text-[10px] uppercase bordercon dual light/dark — Bozza→amber, Pubblicato→sky, Accettato→emerald, Rifiutato→red (stesso linguaggio diStatusBadge; label ITA mappate localmente perché gli stati proposal ≠ lead stages). - Azione: solo per
published/acceptedlink "Apri →" (ArrowRightcon group-hover translate) al preventivo pubblico/preventivo/<slug>in new tab; altrimenti—muted. - Empty state:
border-dashed border-border rounded-xl,FileTextmuted + CTA "Genera il primo preventivo →". Footer "Mostrando N di N preventivi generati".
Progetti (lista) notes
/admin/projects migrata a "Quiet Luxury" (replica design-reference/pagina-progetti.html).
Stesso contenitore tabella di Preventivi (bg-card rounded-xl border-border shadow-card,
header bg-muted/50 uppercase muted, righe hover:bg-muted/40 + divide-y divide-border).
- Header:
PageHeader"Progetti" + sottotitolo (redditività oraria), azioneButton"Nuovo Progetto" (+, link a/admin/projects/new). - Colonne: Progetto (link → dettaglio + cliente muted, badge "Archiviato" se archiviato),
Valore Totale (
text-right font-mono), Acconto (badge centrato), Saldo (badge centrato), Timer (TimerCellcentrato), Redditività €/H (text-right). - Badge pagamento (
ProjectRow): pill rounded-fulltext-[10px] uppercase borderdual light/dark — saldato→emerald, da_saldare→rose, inviata→amber; prefissati "Acconto:"/"Saldo:". Pagamento assente →—muted. - TimerCell: ora una pill
rounded-full border font-mono— idle neutro (bg-muted, play triangle subg-foreground/10), running emerald (bg-emerald-50/60darkemerald-950/30, pausa subg-emerald-500), contatore live. Stessa pill riusata nelTimerTabdel dettaglio progetto. - Redditività:
null(0h tracciate) →—muted;>0→ emerald bold con dotbg-emerald-500;=0→ mono muted. Footer "Mostrando N di N progetti attivi".
Impostazioni notes
/admin/impostazioni migrata a "Quiet Luxury" (replica design-reference/pagina-impostazioni.html).
Pagina a sezioni impilate (space-y-8), ogni sezione è una card
rounded-xl border-border-light bg-card shadow-card p-6.
- Header:
PageHeader"Impostazioni" + sottotitolo ("Configura le tariffe target e gestisci le tassonomie globali del sistema"). - Titoli sezione:
text-xs font-bold uppercase tracking-wider text-foreground, sottotitolotext-xs text-tertiary. Le sezioni tassonomia hanno un'icona (Tag/BookOpen) in un boxh-9 w-9 rounded-lg border-border-light bg-muted text-muted-foreground. - Analytics Profittabilità: form server-action con input
type=numberin un gruppo bordatofocus-within:border-primary focus-within:ring-1 ring-primary, prefisso€/suffisso/hmuted assoluti, valorefont-mono; bottone "Salva"bg-primary text-primary-foreground. - Griglie tassonomia (
TaxonomyManager):border-t border-border-light pt-4+ gridgap-6— Offertemd:grid-cols-2, Catalogomd:grid-cols-2 lg:grid-cols-3. - PoolManager (box campo):
rounded-xl border-border-light bg-muted/40 p-5 flex flex-col justify-between. Testata = label uppercase muted + badge conteggio mono (bg-card border-border-light text-tertiary, sempre visibile anche a 0). Valori = chipbg-card border-border/60 text-foreground shadow-smconX(hover destructive → cascade delete confermata viawindow.confirm). Vuoto → "Nessun valore inserito" italic muted. Aggiunta = input full-widthbg-card border-bordercon Enter (no bottone+); duplicati bloccati con errore inlinetext-destructive. - Footer: nota
text-xs text-tertiarysulla sincronizzazione bidirezionale dei valori.
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). - Stato dei task (
TaskStatusIndicator): le cinque forme si distinguono senza colore —todocerchio vuotoborder-border,in_progresscerchio + punto ambra,in_reviewcerchio + spunta vuota violetta,donecerchio pieno- spunta emerald,
cancelledcerchio spento + X. La spunta compare a lavoro finito e si riempie a lavoro confermato; la X è l'unica forma che non è un passo avanti. La pill di testo compare suin_progress,in_reviewecancelled;todoedonesi leggono dalla forma e dal titolo.doneecancelledsono entrambi barrati, ed è proprio per questo checancelledporta la pill: la differenza fra consegnato e tolto dal lavoro è troppo importante per affidarla al solo disegno dentro il cerchio. Colori come le altre pill del portale (soft tint-50/700+ variantedark:, tintamutedper la cancellata), tutti raccolti inTASK_STATUS_VISUALSe mai riscritti nei componenti: le stesse tinte colorano le colonne del Kanban cliente e la legenda, che si genera daTASK_STATUSES. L'icona èaria-hiddene lo stato viaggia in unosr-only, così lo screen reader lo annuncia anche su quelli senza pill. La colonna «Cancellate» del Kanban cliente è l'unica che si nasconde da vuota (le altre quattro si tengono la larghezza) — mai se è piena: nessun task deve sparire dalla board.
- spunta emerald,
- Box pagamenti (
PaymentStatus): sopra le righe, un riquadro «Prossimo pagamento» con data lunga e conto alla rovescia in parole («tra 12 giorni», «domani», «oggi»); se la scadenza è passata diventa rosso e dice «Pagamento scaduto — scaduto da N giorni». Ogni riga porta la sua data sotto l'etichetta: «Scade il…» se aperta, «Pagato il…» se saldata, nulla se in gestionale la data non c'è — il portale non inventa scadenze. Nessun importo per riga (LOCKED #2): le date non sono cifre. I giorni si contano insrc/lib/payment-dates.tssui giorni civili a Roma, non sugli istanti. - Sidebar cards (
OffersSection,PaymentStatus,DocumentsSection,NotesSection,TranscriptsSection) all followrounded-xl border-border-light bg-card shadow-card. - Offer card (
OffersSection): due sole righe di importo — «Valore dell'offerta» (override admin o somma dei servizi; nascosta a 0) e «Canone mensile» / «Investimento finale». Niente accordion «Cosa è compreso»: la lista servizi non arriva più al client, e il componente non è più"use client". 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.