feat: Client Portal redesign to Quiet Luxury + milestone stepper

- New MilestoneStepper primitive (horizontal per-phase progress)
- Token-migrate portal shell, phase cards, sidebar cards, kanban to dual-theme
- Soft-tint status pills (emerald/amber/muted) replacing solid badges
- embedded prop on ClientDashboard fixes double-header in multi-project tabs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:54:58 +02:00
parent 6d5e04bf9c
commit 11870e15d3
13 changed files with 333 additions and 306 deletions
+17 -20
View File
@@ -7,29 +7,29 @@ type Task = ClientView["phases"][number]["tasks"][number] & {
phaseTitle: string;
};
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string; dotClass: string; headerClass: string }[] = [
{ id: "todo", label: "Da fare", dotClass: "bg-[#d4d4d8]", headerClass: "text-[#71717a]" },
{ id: "in_progress", label: "In corso", dotClass: "bg-[#DEF168]", headerClass: "text-[#1A463C]" },
{ id: "done", label: "Fatto", dotClass: "bg-[#1A463C]", headerClass: "text-[#1A463C]" },
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string }[] = [
{ id: "todo", label: "Da fare" },
{ id: "in_progress", label: "In corso" },
{ id: "done", label: "Fatto" },
];
function TaskCard({ task, token }: { task: Task; token: string }) {
return (
<div className="bg-white rounded-lg border border-[#e5e7eb] px-3 py-2.5 shadow-sm">
<p className="text-[10px] font-medium text-[#71717a] uppercase tracking-wide mb-1 truncate">
<div className="bg-card rounded-lg border border-border-light px-4 py-4 shadow-sm hover:border-primary/30 hover:shadow-card-hover transition-all">
<p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wide mb-1 truncate">
{task.phaseTitle}
</p>
<p className={`text-sm font-medium leading-snug mb-2 ${task.status === "done" ? "line-through text-[#71717a]" : "text-[#1a1a1a]"}`}>
<p className={`text-xs font-medium leading-snug ${task.status === "done" ? "line-through text-muted-foreground" : "text-foreground"}`}>
{task.title}
</p>
{task.description && (
<p className="text-xs text-[#71717a] mb-2 leading-snug">{task.description}</p>
<p className="text-[11px] text-muted-foreground mt-2 leading-snug">{task.description}</p>
)}
{task.deliverables.length > 0 && (
<ul className="space-y-1.5 mt-2 pt-2 border-t border-[#f4f4f5]">
<ul className="space-y-1.5 mt-2 pt-2 border-t border-border-light">
{task.deliverables.map((d) => (
<li key={d.id} className="flex items-center justify-between gap-2">
<span className="text-xs text-[#666666] truncate">{d.title}</span>
<span className="text-xs text-muted-foreground truncate">{d.title}</span>
{(d.status === "pending" || d.status === "submitted" || d.approved_at !== null) && (
<ApproveButton deliverableId={d.id} token={token} approvedAt={d.approved_at} />
)}
@@ -53,24 +53,21 @@ export function ClientKanban({ phases, token }: { phases: ClientView["phases"];
};
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
{COLUMNS.map((col) => (
<div key={col.id} className="flex flex-col rounded-xl border border-[#e5e7eb] bg-[#f9f9f9] min-h-[200px]">
<div className={`px-4 py-3 flex items-center justify-between ${col.headerClass}`}>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${col.dotClass}`} />
<span className="text-xs font-bold uppercase tracking-wider">{col.label}</span>
</div>
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
<div key={col.id} className="flex flex-col gap-4 rounded-xl border border-border-light bg-muted/60 p-4 min-h-[300px]">
<div className="flex items-center justify-between border-b border-border-light pb-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{col.label}</span>
<span className="text-[10px] font-bold tabular-nums bg-secondary text-secondary-foreground rounded-full px-2 py-0.5">
{tasksByStatus[col.id].length}
</span>
</div>
<div className="flex-1 p-3 space-y-2">
<div className="flex-1 flex flex-col gap-3">
{tasksByStatus[col.id].map((task) => (
<TaskCard key={task.id} task={task} token={token} />
))}
{tasksByStatus[col.id].length === 0 && (
<p className="text-xs text-[#d4d4d8] italic text-center py-10">Nessun task</p>
<p className="text-xs text-muted-foreground italic text-center py-10">Nessun task</p>
)}
</div>
</div>
@@ -18,20 +18,20 @@ export function PhaseViewToggle({
return (
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-[#1a1a1a]">Fasi del Progetto</h2>
<div className="flex items-center gap-1 bg-[#f4f4f5] rounded-lg p-1">
<h2 className="text-lg font-bold text-foreground tracking-tight">Fasi del Progetto</h2>
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
<button
onClick={() => setView("timeline")}
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
view === "timeline" ? "bg-white text-[#1A463C] shadow-sm" : "text-[#71717a] hover:text-[#1a1a1a]"
className={`px-5 py-1.5 rounded-md text-[11px] font-semibold transition-all ${
view === "timeline" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
}`}
>
Timeline
</button>
<button
onClick={() => setView("kanban")}
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
view === "kanban" ? "bg-white text-[#1A463C] shadow-sm" : "text-[#71717a] hover:text-[#1a1a1a]"
className={`px-5 py-1.5 rounded-md text-[11px] font-semibold transition-all ${
view === "kanban" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
}`}
>
Kanban