feat: Sprint 2 — data storytelling and visual richness

Timeline project color system:
- 16-color deterministic palette (same project = same color always)
- Resource panel: allocation blocks colored by project instead of uniform green
- Project panel: colored left border + dot on project headers
- ProjectColorLegend: floating strip showing color-to-project mapping
- Utilization intensity tint: subtle background gradient on resource rows

Table visual enhancements:
- Resources: inline 3px utilization bar below chargeability percentage
- Resources: 32px avatar circles with initials + role-derived colors
- Projects: animated budget bars, styled resource count badges
- Allocations: 3px left border colored by status (green/amber/blue/gray/red)

KPI progress rings:
- Budget utilization: ProgressRing wrapping AnimatedNumber on dashboard
- Chargeability report: ring on average chargeability summary card
- Resource detail: rings on chargeability target + actual metrics
- Vacation balance: ring showing remaining days with color thresholds
- Demand widget: mini rings on FTE fill rate per project
- Resource detail: FadeIn on SkillRadarChart

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-19 00:58:06 +01:00
parent ae92923c28
commit a97597093f
13 changed files with 399 additions and 53 deletions
+58
View File
@@ -0,0 +1,58 @@
/**
* Deterministic project color assignment.
* Maps a project ID to one of 16 visually distinct colors.
* Colors are chosen for readability on both light and dark backgrounds.
*/
const PROJECT_PALETTE = [
{ bg: "bg-sky-500/70", dark: "bg-sky-400/60", border: "border-sky-600", hex: "#0ea5e9" },
{ bg: "bg-violet-500/70", dark: "bg-violet-400/60", border: "border-violet-600", hex: "#8b5cf6" },
{ bg: "bg-amber-500/70", dark: "bg-amber-400/60", border: "border-amber-600", hex: "#f59e0b" },
{ bg: "bg-rose-500/70", dark: "bg-rose-400/60", border: "border-rose-600", hex: "#f43f5e" },
{ bg: "bg-emerald-500/70", dark: "bg-emerald-400/60", border: "border-emerald-600", hex: "#10b981" },
{ bg: "bg-indigo-500/70", dark: "bg-indigo-400/60", border: "border-indigo-600", hex: "#6366f1" },
{ bg: "bg-orange-500/70", dark: "bg-orange-400/60", border: "border-orange-600", hex: "#f97316" },
{ bg: "bg-teal-500/70", dark: "bg-teal-400/60", border: "border-teal-600", hex: "#14b8a6" },
{ bg: "bg-pink-500/70", dark: "bg-pink-400/60", border: "border-pink-600", hex: "#ec4899" },
{ bg: "bg-cyan-500/70", dark: "bg-cyan-400/60", border: "border-cyan-600", hex: "#06b6d4" },
{ bg: "bg-lime-500/70", dark: "bg-lime-400/60", border: "border-lime-600", hex: "#84cc16" },
{ bg: "bg-fuchsia-500/70", dark: "bg-fuchsia-400/60", border: "border-fuchsia-600", hex: "#d946ef" },
{ bg: "bg-yellow-500/70", dark: "bg-yellow-400/60", border: "border-yellow-600", hex: "#eab308" },
{ bg: "bg-red-500/70", dark: "bg-red-400/60", border: "border-red-600", hex: "#ef4444" },
{ bg: "bg-blue-500/70", dark: "bg-blue-400/60", border: "border-blue-600", hex: "#3b82f6" },
{ bg: "bg-green-500/70", dark: "bg-green-400/60", border: "border-green-600", hex: "#22c55e" },
] as const;
export type ProjectColor = (typeof PROJECT_PALETTE)[number];
/**
* Returns a deterministic color for a project ID using a simple hash.
* Same project ID always gets the same color.
*/
export function getProjectColor(projectId: string): ProjectColor {
let hash = 0;
for (let i = 0; i < projectId.length; i++) {
hash = ((hash << 5) - hash + projectId.charCodeAt(i)) | 0;
}
return PROJECT_PALETTE[Math.abs(hash) % PROJECT_PALETTE.length]!;
}
/**
* Returns the hex color with an alpha suffix for inline styles.
* Useful when the block uses inline `backgroundColor` instead of Tailwind classes.
*/
export function getProjectHex(projectId: string, alpha = "B3"): string {
return getProjectColor(projectId).hex + alpha;
}
/**
* Build a lookup map for a set of project IDs.
* Call once per render with visible project IDs.
*/
export function buildProjectColorMap(projectIds: string[]): Map<string, ProjectColor> {
const map = new Map<string, ProjectColor>();
for (const id of projectIds) {
map.set(id, getProjectColor(id));
}
return map;
}