style: refine Pipeline fidelity to Quiet Luxury reference
- Sidebar: revert to white palette (drop emerald pass), remove right border - Table: softer slate-100 borders, remove min-height gap, "Mostrando N di M lead" footer, empty Tag cell = dashed-circle add button - SegmentedToggle: more visible track (slate-200/60), neutral active text - Kanban: roomy fixed-width columns (280px, horizontal scroll), sober column headers (no colored dots), airy cards matching mock (name+badge/email/ company+"Tag +"); 6 stages + @dnd-kit intact Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 min-h-screen bg-[#1A463C] border-r border-emerald-950/20 flex flex-col overflow-hidden",
|
"shrink-0 min-h-screen bg-[#1A463C] flex flex-col overflow-hidden",
|
||||||
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
||||||
collapsed ? "w-16" : "w-[200px]"
|
collapsed ? "w-16" : "w-[200px]"
|
||||||
)}
|
)}
|
||||||
@@ -54,7 +54,7 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<span className="font-bold text-emerald-50 tracking-tight text-sm whitespace-nowrap">
|
<span className="font-bold text-white tracking-tight text-sm whitespace-nowrap">
|
||||||
iamcavalli
|
iamcavalli
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -73,14 +73,14 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
"flex items-center py-2.5 rounded-lg text-sm transition-all duration-200",
|
"flex items-center py-2.5 rounded-lg text-sm transition-all duration-200",
|
||||||
rowClass,
|
rowClass,
|
||||||
active
|
active
|
||||||
? "bg-white/[0.08] text-white font-medium shadow-sm"
|
? "bg-white/15 text-white font-medium"
|
||||||
: "text-emerald-200/70 hover:text-white hover:bg-white/5"
|
: "text-white/65 hover:text-white hover:bg-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth={1.8}
|
strokeWidth={1.8}
|
||||||
className={cn("shrink-0", active && "text-emerald-400")}
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
{!collapsed && <span className="whitespace-nowrap">{label}</span>}
|
{!collapsed && <span className="whitespace-nowrap">{label}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -96,7 +96,7 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
collapsed ? "justify-center px-0" : "justify-between px-3"
|
collapsed ? "justify-center px-0" : "justify-between px-3"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!collapsed && <span className="text-xs text-emerald-200/50">Tema</span>}
|
{!collapsed && <span className="text-xs text-white/50">Tema</span>}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -104,7 +104,7 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
|||||||
title={collapsed ? "Esci" : undefined}
|
title={collapsed ? "Esci" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center py-2.5 w-full rounded-lg text-sm transition-all duration-200",
|
"flex items-center py-2.5 w-full rounded-lg text-sm transition-all duration-200",
|
||||||
"text-red-300/80 hover:text-red-200 hover:bg-red-950/20",
|
"text-white/65 hover:text-white hover:bg-white/10",
|
||||||
rowClass
|
rowClass
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -191,16 +191,21 @@ function LeadRow({
|
|||||||
export function LeadTable({
|
export function LeadTable({
|
||||||
leads,
|
leads,
|
||||||
options,
|
options,
|
||||||
|
total,
|
||||||
}: {
|
}: {
|
||||||
leads: LeadWithTags[];
|
leads: LeadWithTags[];
|
||||||
options: LeadFieldOptions;
|
options: LeadFieldOptions;
|
||||||
|
total?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const count = leads.length;
|
||||||
|
const totalCount = total ?? leads.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card rounded-xl border border-border shadow-card overflow-hidden">
|
<div className="bg-card rounded-xl border border-border-light shadow-card overflow-hidden">
|
||||||
<div className="overflow-x-auto min-h-[300px]">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border bg-muted/50 text-muted-foreground text-[11px] font-semibold uppercase tracking-wider">
|
<tr className="border-b border-border-light bg-muted/50 text-muted-foreground text-[11px] font-semibold uppercase tracking-wider">
|
||||||
{COLUMN_HEADERS.map((header) => (
|
{COLUMN_HEADERS.map((header) => (
|
||||||
<th key={header} className={cn("py-4 px-6", HEADER_ALIGN[header])}>
|
<th key={header} className={cn("py-4 px-6", HEADER_ALIGN[header])}>
|
||||||
{header}
|
{header}
|
||||||
@@ -208,7 +213,7 @@ export function LeadTable({
|
|||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border text-sm">
|
<tbody className="divide-y divide-border-light text-sm">
|
||||||
{leads.map((lead) => (
|
{leads.map((lead) => (
|
||||||
<LeadRow key={lead.id} lead={lead} options={options} />
|
<LeadRow key={lead.id} lead={lead} options={options} />
|
||||||
))}
|
))}
|
||||||
@@ -220,8 +225,8 @@ export function LeadTable({
|
|||||||
<div className="text-center py-8 text-muted-foreground">Nessun lead trovato</div>
|
<div className="text-center py-8 text-muted-foreground">Nessun lead trovato</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="border-t border-border px-6 py-4 text-xs text-muted-foreground">
|
<div className="border-t border-border-light px-6 py-4 text-xs text-muted-foreground">
|
||||||
Mostrando {leads.length} lead
|
Mostrando {count} di {totalCount} lead
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,26 +22,22 @@ type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "
|
|||||||
const LEAD_COLUMNS: {
|
const LEAD_COLUMNS: {
|
||||||
id: LeadStage;
|
id: LeadStage;
|
||||||
label: string;
|
label: string;
|
||||||
headerClass: string;
|
|
||||||
dotClass: string;
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{ id: "contacted", label: "Contattato", headerClass: "text-muted-foreground", dotClass: "bg-blue-400 dark:bg-blue-500" },
|
{ id: "contacted", label: "Contattato" },
|
||||||
{ id: "qualified", label: "Qualificato", headerClass: "text-purple-700 dark:text-purple-400", dotClass: "bg-purple-400 dark:bg-purple-500" },
|
{ id: "qualified", label: "Qualificato" },
|
||||||
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700 dark:text-amber-400", dotClass: "bg-amber-400 dark:bg-amber-500" },
|
{ id: "proposal_sent", label: "Offerta inviata" },
|
||||||
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700 dark:text-orange-400", dotClass: "bg-orange-400 dark:bg-orange-500" },
|
{ id: "negotiating", label: "Trattativa" },
|
||||||
{ id: "won", label: "Vinto", headerClass: "text-emerald-700 dark:text-emerald-400", dotClass: "bg-emerald-500" },
|
{ id: "won", label: "Vinto" },
|
||||||
{ id: "lost", label: "Perso", headerClass: "text-red-700 dark:text-red-400", dotClass: "bg-red-400 dark:bg-red-500" },
|
{ id: "lost", label: "Perso" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[];
|
const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[];
|
||||||
|
|
||||||
function DroppableColumn({
|
function DroppableColumn({
|
||||||
id, label, headerClass, dotClass, leads, activeId,
|
id, label, leads, activeId,
|
||||||
}: {
|
}: {
|
||||||
id: LeadStage;
|
id: LeadStage;
|
||||||
label: string;
|
label: string;
|
||||||
headerClass: string;
|
|
||||||
dotClass: string;
|
|
||||||
leads: LeadWithTags[];
|
leads: LeadWithTags[];
|
||||||
activeId: string | null;
|
activeId: string | null;
|
||||||
}) {
|
}) {
|
||||||
@@ -50,21 +46,18 @@ function DroppableColumn({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`flex flex-col rounded-xl border p-4 transition-colors ${
|
className={`w-[280px] shrink-0 flex flex-col gap-4 rounded-xl border p-4 transition-colors min-h-[420px] ${
|
||||||
isOver ? "border-primary bg-primary/5" : "border-border bg-muted/60"
|
isOver ? "border-primary bg-primary/5" : "border-border-light bg-muted/60"
|
||||||
} min-h-[240px]`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center justify-between pb-2 mb-2 border-b border-border ${headerClass}`}>
|
<div className="flex items-center justify-between border-b border-border-light pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{label}</span>
|
||||||
<span className={`w-2 h-2 rounded-full ${dotClass}`} />
|
<span className="text-xs font-medium tabular-nums bg-secondary text-muted-foreground px-2 py-0.5 rounded-full">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">{label}</span>
|
|
||||||
</div>
|
|
||||||
<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}
|
{leads.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
{leads.map((lead) => (
|
{leads.map((lead) => (
|
||||||
<DraggableLeadCard key={lead.id} lead={lead} status={id} isActive={activeId === lead.id} />
|
<DraggableLeadCard key={lead.id} lead={lead} status={id} isActive={activeId === lead.id} />
|
||||||
))}
|
))}
|
||||||
@@ -99,23 +92,21 @@ function DraggableLeadCard({
|
|||||||
style={style}
|
style={style}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
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 ${
|
className={`bg-card p-4 rounded-lg border border-border shadow-sm cursor-grab active:cursor-grabbing select-none transition-all duration-200 ${
|
||||||
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
|
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex justify-between items-start gap-2 mb-2">
|
||||||
<p className="text-sm font-medium text-foreground">{lead.name}</p>
|
<p className="text-xs font-semibold text-foreground">{lead.name}</p>
|
||||||
<StatusBadge status={status} className="rounded px-1.5 py-0.5 text-[9px]" />
|
<StatusBadge status={status} className="rounded px-1.5 py-0.5 text-[9px]" />
|
||||||
</div>
|
</div>
|
||||||
{lead.email && (
|
{lead.email && (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{lead.email}</p>
|
<p className="text-[11px] text-muted-foreground mb-3 line-clamp-1">{lead.email}</p>
|
||||||
)}
|
|
||||||
{lead.company && (
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">{lead.company}</p>
|
|
||||||
)}
|
|
||||||
{lead.next_action && (
|
|
||||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{lead.next_action}</p>
|
|
||||||
)}
|
)}
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-muted-foreground">
|
||||||
|
<span className="font-medium">{lead.company ?? ""}</span>
|
||||||
|
<span>Tag +</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -171,7 +162,7 @@ export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="grid grid-cols-6 gap-3 min-w-[1080px]">
|
<div className="flex gap-4">
|
||||||
{LEAD_COLUMNS.map((col) => (
|
{LEAD_COLUMNS.map((col) => (
|
||||||
<DroppableColumn
|
<DroppableColumn
|
||||||
key={col.id}
|
key={col.id}
|
||||||
@@ -185,10 +176,10 @@ export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
|
|||||||
|
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay dropAnimation={null}>
|
||||||
{activeLead && (
|
{activeLead && (
|
||||||
<div className="bg-card rounded-lg border-2 border-primary px-3 py-2.5 shadow-xl rotate-1 pointer-events-none">
|
<div className="bg-card rounded-lg border-2 border-primary p-4 shadow-xl rotate-1 pointer-events-none">
|
||||||
<p className="text-sm font-medium text-foreground">{activeLead.name}</p>
|
<p className="text-xs font-semibold text-foreground mb-2">{activeLead.name}</p>
|
||||||
{activeLead.company && (
|
{activeLead.company && (
|
||||||
<p className="text-xs text-muted-foreground">{activeLead.company}</p>
|
<p className="text-[10px] text-muted-foreground font-medium">{activeLead.company}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function LeadsViewToggle({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{view === "list" ? (
|
{view === "list" ? (
|
||||||
<LeadTable leads={filtered} options={options} />
|
<LeadTable leads={filtered} total={leads.length} options={options} />
|
||||||
) : (
|
) : (
|
||||||
<LeadsKanbanBoard leads={filtered} />
|
<LeadsKanbanBoard leads={filtered} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function SegmentedToggle<T extends string>({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center gap-1 bg-muted rounded-lg p-1 w-fit", className)}>
|
<div className={cn("flex items-center gap-1 bg-slate-200/60 dark:bg-white/5 rounded-lg p-1 w-fit", className)}>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const active = option.value === value;
|
const active = option.value === value;
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +32,7 @@ export function SegmentedToggle<T extends string>({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"px-3 py-1.5 rounded-md text-xs font-semibold transition-all duration-200",
|
"px-3 py-1.5 rounded-md text-xs font-semibold transition-all duration-200",
|
||||||
active
|
active
|
||||||
? "bg-card text-primary shadow-sm"
|
? "bg-card text-foreground shadow-sm"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,37 +81,42 @@ export function OptionMultiSelect({
|
|||||||
onClick={() => !disabled && setIsOpen((v) => !v)}
|
onClick={() => !disabled && setIsOpen((v) => !v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap gap-1 items-center px-2 py-1 rounded transition-colors duration-150 min-h-[28px]",
|
"flex flex-wrap gap-1 items-center px-2 py-1 rounded transition-colors duration-150 min-h-[28px]",
|
||||||
|
values.length === 0 && "justify-center",
|
||||||
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-muted"
|
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{values.length === 0 ? (
|
{values.length === 0 ? (
|
||||||
<span className="text-xs text-muted-foreground">—</span>
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full border border-dashed border-border text-muted-foreground hover:text-foreground hover:border-muted-foreground transition-all">
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
values.map((v) => (
|
<>
|
||||||
<Badge
|
{values.map((v) => (
|
||||||
key={v}
|
<Badge
|
||||||
variant="outline"
|
key={v}
|
||||||
className={cn(
|
variant="outline"
|
||||||
getOptionColor(v),
|
className={cn(
|
||||||
"border-transparent text-xs px-2 py-0.5 gap-1 font-medium"
|
getOptionColor(v),
|
||||||
)}
|
"border-transparent text-xs px-2 py-0.5 gap-1 font-medium"
|
||||||
>
|
)}
|
||||||
{v}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove(v);
|
|
||||||
}}
|
|
||||||
className="hover:opacity-70"
|
|
||||||
aria-label={`Rimuovi ${v}`}
|
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
{v}
|
||||||
</button>
|
<button
|
||||||
</Badge>
|
type="button"
|
||||||
))
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove(v);
|
||||||
|
}}
|
||||||
|
className="hover:opacity-70"
|
||||||
|
aria-label={`Rimuovi ${v}`}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|||||||
Reference in New Issue
Block a user