feat: Quiet Luxury design system + Pipeline (ex-Lead) redesign

- Design system foundations: Plus Jakarta Sans font, shadow-card/radius
  tokens in @theme, design-reference/DESIGN-SYSTEM.md with component inventory
- Collapsible admin shell (AdminShell): smooth w-64<->w-20 sidebar, new top
  header with "Admin" + avatar placeholder, localStorage-persisted state
- Rename route /admin/leads -> /admin/pipeline (redirect stubs preserved),
  nav label Lead -> Pipeline; DB table unchanged
- Reusable primitives: StatusBadge, SearchInput, SegmentedToggle (dual-theme)
- Luxury restyle of lead table, kanban (6 stages + @dnd-kit intact), PageHeader
- Tokenize editable-cell / option-multi-select for dark-mode legibility

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:16:44 +02:00
parent 43cb7e7469
commit 86e1499e8f
34 changed files with 3134 additions and 231 deletions
+21 -21
View File
@@ -13,7 +13,7 @@ import {
useDroppable,
useDraggable,
} from "@dnd-kit/core";
import { updateLeadField } from "@/app/admin/leads/actions";
import { updateLeadField } from "@/app/admin/pipeline/actions";
import type { LeadWithTags } from "@/lib/admin-queries";
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
@@ -24,12 +24,12 @@ const LEAD_COLUMNS: {
headerClass: string;
dotClass: string;
}[] = [
{ id: "contacted", label: "Contattato", headerClass: "text-[#71717a]", dotClass: "bg-[#d4d4d8]" },
{ id: "qualified", label: "Qualificato", headerClass: "text-purple-700", dotClass: "bg-purple-400" },
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700", dotClass: "bg-amber-400" },
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700", dotClass: "bg-orange-400" },
{ id: "won", label: "Vinto", headerClass: "text-green-700", dotClass: "bg-green-500" },
{ id: "lost", label: "Perso", headerClass: "text-red-700", dotClass: "bg-red-400" },
{ id: "contacted", label: "Contattato", headerClass: "text-muted-foreground", dotClass: "bg-blue-400 dark:bg-blue-500" },
{ id: "qualified", label: "Qualificato", headerClass: "text-purple-700 dark:text-purple-400", dotClass: "bg-purple-400 dark:bg-purple-500" },
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700 dark:text-amber-400", dotClass: "bg-amber-400 dark:bg-amber-500" },
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700 dark:text-orange-400", dotClass: "bg-orange-400 dark:bg-orange-500" },
{ id: "won", label: "Vinto", headerClass: "text-emerald-700 dark:text-emerald-400", dotClass: "bg-emerald-500" },
{ id: "lost", label: "Perso", headerClass: "text-red-700 dark:text-red-400", dotClass: "bg-red-400 dark:bg-red-500" },
];
const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[];
@@ -49,26 +49,26 @@ function DroppableColumn({
return (
<div
ref={setNodeRef}
className={`flex flex-col rounded-xl border-2 transition-colors ${
isOver ? "border-[#1A463C] bg-[#1A463C]/5" : "border-[#e5e7eb] bg-[#f9f9f9]"
className={`flex flex-col rounded-xl border p-4 transition-colors ${
isOver ? "border-primary bg-primary/5" : "border-border bg-muted/60"
} min-h-[240px]`}
>
<div className={`px-4 py-3 flex items-center justify-between ${headerClass}`}>
<div className={`flex items-center justify-between pb-2 mb-2 border-b border-border ${headerClass}`}>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${dotClass}`} />
<span className="text-xs font-bold uppercase tracking-wider">{label}</span>
</div>
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
<span className="text-xs font-semibold tabular-nums bg-card rounded-full px-2 py-0.5 border border-border text-muted-foreground">
{leads.length}
</span>
</div>
<div className="flex-1 p-3 space-y-2">
<div className="flex-1 space-y-2">
{leads.map((lead) => (
<DraggableLeadCard key={lead.id} lead={lead} isActive={activeId === lead.id} />
))}
{leads.length === 0 && (
<p className="text-xs text-[#d4d4d8] italic text-center py-10 select-none">
<p className="text-xs text-muted-foreground italic text-center py-10 select-none">
Nessun lead
</p>
)}
@@ -90,16 +90,16 @@ function DraggableLeadCard({ lead, isActive }: { lead: LeadWithTags; isActive: b
style={style}
{...listeners}
{...attributes}
className={`bg-white rounded-lg border border-[#e5e7eb] px-3 py-2.5 cursor-grab active:cursor-grabbing shadow-sm select-none transition-opacity ${
isDragging ? "opacity-30" : "hover:border-[#1A463C]/40 hover:shadow"
className={`bg-card rounded-lg border border-border px-3 py-2.5 cursor-grab active:cursor-grabbing shadow-sm select-none transition-all duration-200 ${
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
}`}
>
<p className="text-sm font-medium text-[#1a1a1a]">{lead.name}</p>
<p className="text-sm font-medium text-foreground">{lead.name}</p>
{lead.company && (
<p className="text-xs text-[#71717a]">{lead.company}</p>
<p className="text-xs text-muted-foreground">{lead.company}</p>
)}
{lead.next_action && (
<p className="text-xs text-[#71717a] mt-1 line-clamp-1">{lead.next_action}</p>
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{lead.next_action}</p>
)}
</div>
);
@@ -170,10 +170,10 @@ export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
<DragOverlay dropAnimation={null}>
{activeLead && (
<div className="bg-white rounded-lg border-2 border-[#1A463C] px-3 py-2.5 shadow-xl rotate-1 pointer-events-none">
<p className="text-sm font-medium text-[#1a1a1a]">{activeLead.name}</p>
<div className="bg-card rounded-lg border-2 border-primary px-3 py-2.5 shadow-xl rotate-1 pointer-events-none">
<p className="text-sm font-medium text-foreground">{activeLead.name}</p>
{activeLead.company && (
<p className="text-xs text-[#71717a]">{activeLead.company}</p>
<p className="text-xs text-muted-foreground">{activeLead.company}</p>
)}
</div>
)}