90
+ ? "var(--color-red-500, #ef4444)"
+ : pct >= 70
+ ? "var(--color-amber-500, #f59e0b)"
+ : "var(--color-emerald-500, #10b981)";
+
return (
-
- Vacation Balance {year}
-
+
+
+ {balance.remainingDays}d
+
+
+
+ Vacation Balance {year}
+
+
{balance.usedDays} of {balance.entitledDays} days used
+
+
{balance.carryoverDays > 0 && (
+{balance.carryoverDays}d carried over
)}
-
-
+
+
diff --git a/apps/web/src/lib/project-colors.ts b/apps/web/src/lib/project-colors.ts
new file mode 100644
index 0000000..5a78d97
--- /dev/null
+++ b/apps/web/src/lib/project-colors.ts
@@ -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
{
+ const map = new Map();
+ for (const id of projectIds) {
+ map.set(id, getProjectColor(id));
+ }
+ return map;
+}