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
+45 -59
View File
@@ -1,9 +1,6 @@
"use client";
import { useState } from "react";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { ApproveButton } from "@/components/client/ApproveButton";
import { useChatContext } from "@/components/client/ChatProvider";
import type { ClientView } from "@/lib/client-view";
@@ -16,53 +13,38 @@ const phaseStatusLabel: Record<"upcoming" | "active" | "done", string> = {
done: "Completata",
};
// Soft-tint status pill — replica del mock ("Quiet Luxury")
const phaseStatusStyle: Record<"upcoming" | "active" | "done", string> = {
upcoming: "border-transparent bg-[#999999] text-white",
active: "border-transparent bg-[#3b82f6] text-white",
done: "border-transparent bg-[#1A463C] text-white",
upcoming: "bg-muted text-muted-foreground border border-border",
active:
"bg-amber-50 text-amber-700 border border-amber-100 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20",
done: "bg-emerald-50 text-emerald-700 border border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20",
};
function PhaseStatusIcon({ status }: { status: "upcoming" | "active" | "done" }) {
if (status === "done") {
return (
<svg className="w-5 h-5 text-[#1A463C]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
</svg>
);
}
if (status === "active") {
return (
<svg className="w-5 h-5 text-[#3b82f6]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
</svg>
);
}
return (
<svg className="w-5 h-5 text-[#999999]" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
<circle cx={12} cy={12} r={9} />
</svg>
);
}
// Colore della barra di avanzamento per stato fase
const phaseBarColor: Record<"upcoming" | "active" | "done", string> = {
upcoming: "bg-border",
active: "bg-amber-500",
done: "bg-emerald-600",
};
function TaskStatusIcon({ status }: { status: "todo" | "in_progress" | "done" }) {
if (status === "done") {
return (
<svg className="w-4 h-4 text-[#1A463C] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
</svg>
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-[10px] font-bold text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400">
</span>
);
}
if (status === "in_progress") {
return (
<svg className="w-4 h-4 text-[#3b82f6] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
</svg>
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-amber-400 bg-card">
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
</span>
);
}
return (
<svg className="w-4 h-4 text-[#999999] shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
<circle cx={12} cy={12} r={9} />
</svg>
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-border bg-card" />
);
}
@@ -80,7 +62,7 @@ export function PhaseCard({
const doneCount = phase.tasks.filter((t) => t.status === "done").length;
return (
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-5">
<div className="rounded-xl border border-border-light bg-card p-6 shadow-card">
{/* Header — always visible, click to toggle */}
<button
type="button"
@@ -89,14 +71,13 @@ export function PhaseCard({
aria-expanded={open}
>
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-2.5 min-w-0">
<PhaseStatusIcon status={phase.status} />
<h3 className="text-base font-bold text-[#1a1a1a] leading-snug">{phase.title}</h3>
</div>
<h3 className="text-sm font-bold text-foreground leading-snug min-w-0">{phase.title}</h3>
<div className="flex items-center gap-2 shrink-0">
<Badge className={`text-xs ${phaseStatusStyle[phase.status]}`}>
<span
className={`rounded px-2.5 py-0.5 text-[10px] font-bold uppercase ${phaseStatusStyle[phase.status]}`}
>
{phaseStatusLabel[phase.status]}
</Badge>
</span>
{/* Chat bubble — opens panel pre-tagged with this phase */}
<button
type="button"
@@ -105,14 +86,14 @@ export function PhaseCard({
openChat(phase.id);
}}
aria-label="Apri chat per questa fase"
className="p-1 rounded-md text-[#999999] hover:text-[#1A463C] hover:bg-[#f0f7f4] transition-colors"
className="p-1 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/5 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={1.8} viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M21 12c0 4.418-4.03 8-9 8a9.77 9.77 0 01-4-.83L3 20l1.09-3.27C3.39 15.56 3 13.83 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</button>
<svg
className={`w-4 h-4 text-[#999999] transition-transform ${open ? "rotate-180" : ""}`}
className={`w-4 h-4 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
strokeWidth={2}
@@ -124,38 +105,43 @@ export function PhaseCard({
</div>
</div>
{/* Progress bar — always visible */}
{/* Progress bar — always visible, colore per stato */}
<div className="mb-1">
<div className="flex justify-between items-center mb-1.5">
<p className="text-xs text-[#666666] font-medium">{doneCount} di {phase.tasks.length} task</p>
<p className="text-xs font-semibold text-[#1a1a1a]">{phase.progress_pct}%</p>
<p className="text-xs text-muted-foreground font-medium">{doneCount} di {phase.tasks.length} task</p>
<p className="text-xs font-semibold text-foreground">{phase.progress_pct}%</p>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className={`h-full rounded-full transition-all ${phaseBarColor[phase.status]}`}
style={{ width: `${phase.progress_pct}%` }}
/>
</div>
<Progress value={phase.progress_pct} className="h-1.5" />
</div>
</button>
{/* Collapsible task list */}
{open && (
<div className="mt-4">
<div className="mt-4 border-t border-border-light pt-4">
{phase.tasks.length === 0 ? (
<p className="text-xs text-[#999999] italic">Nessun task ancora configurato.</p>
<p className="text-xs text-muted-foreground italic">Nessun task ancora configurato.</p>
) : (
<ul className="space-y-3">
{phase.tasks.map((task) => (
<li key={task.id} className="flex items-start gap-2.5">
<li key={task.id} className="flex items-start gap-3">
<TaskStatusIcon status={task.status} />
<div className="flex-1 min-w-0">
<p
className={`text-sm leading-snug ${
className={`text-xs leading-snug ${
task.status === "done"
? "line-through text-[#999999]"
: "text-[#1a1a1a]"
? "line-through text-muted-foreground"
: "text-foreground"
}`}
>
{task.title}
</p>
{task.description && (
<p className="text-xs text-[#999999] mt-0.5 leading-snug">
<p className="text-[11px] text-muted-foreground mt-0.5 leading-snug">
{task.description}
</p>
)}
@@ -164,9 +150,9 @@ export function PhaseCard({
{task.deliverables.map((d) => (
<li
key={d.id}
className="bg-[#f9f9f9] rounded px-3 py-2 flex items-center justify-between gap-2"
className="bg-muted rounded-lg px-3 py-2 flex items-center justify-between gap-2"
>
<span className="text-xs text-[#666666] truncate font-medium">
<span className="text-xs text-muted-foreground truncate font-medium">
{d.title}
</span>
{(d.status === "pending" ||
@@ -189,6 +175,6 @@ export function PhaseCard({
)}
</div>
)}
</Card>
</div>
);
}