diff --git a/src/components/RepoHealthPanel.tsx b/src/components/RepoHealthPanel.tsx
new file mode 100644
index 00000000..36b10d9c
--- /dev/null
+++ b/src/components/RepoHealthPanel.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useEffect } from "react";
+import type { RepoHealthScore } from "@/types/repo-health";
+interface Props {
+ health: RepoHealthScore;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+function ScoreBar({ score, maxScore }: { score: number; maxScore: number }) {
+ const pct = Math.round((score / maxScore) * 100);
+ const color = pct >= 70 ? "bg-[var(--accent)]" : pct >= 40 ? "bg-[var(--warning)]" : "bg-[var(--destructive)]";
+ return (
+
+ );
+}
+
+export default function RepoHealthPanel({ health, isOpen, onClose }: Props) {
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+
+ document.addEventListener("keydown", handler);
+
+ return () => {
+ document.removeEventListener("keydown", handler);
+ };
+ }, [onClose]);
+
+ if (!isOpen) return null;
+ const s = health.signals;
+ const shortName = health.repo.split("/")[1] ?? health.repo;
+
+ const commitScore = Math.min(20, Math.round((s.commitFrequency / 10) * 20));
+ const prScore = Math.min(20, Math.round(s.prMergeRate * 25));
+ const prTimeScore = s.avgPrOpenTimeHours === 0 ? 20 : Math.max(0, Math.round(20 - (s.avgPrOpenTimeHours / 168) * 20));
+ const issueScore = Math.max(0, Math.round(20 - s.openIssuesCount * 2));
+ const recencyScore = Math.max(0, Math.round(20 - (s.daysSinceLastCommit / 30) * 20));
+
+ const dimensions = [
+ { label: "Commit Activity", score: commitScore, tip: commitScore < 10 ? "Aim for at least 10 commits per month." : "Good commit frequency!" },
+ { label: "PR Merge Rate", score: prScore, tip: prScore < 10 ? "Review and close stale pull requests." : "Healthy merge rate." },
+ { label: "PR Turnaround", score: prTimeScore, tip: prTimeScore < 10 ? "Try to review and merge PRs faster." : "PRs are moving quickly." },
+ { label: "Issue Load", score: issueScore, tip: issueScore < 10 ? "Triage and close outdated issues." : "Issue backlog looks manageable." },
+ { label: "Recent Activity", score: recencyScore, tip: recencyScore < 10 ? `Last commit was ${s.daysSinceLastCommit} days ago.` : "Repo is actively maintained." },
+ ];
+
+ return (
+
+
+
+
+
+
+ Health Breakdown
+
+
{shortName}
+
+
+
+ {health.score}
+
+
+
+
+
+ {dimensions.map((dim) => (
+
+
+ {dim.label}
+ {dim.score}/20
+
+
+ {dim.score < 20 &&
{dim.tip}
}
+
+ ))}
+
+
+ Score based on activity in the last 30 days. Updates on page refresh.
+
+
+
+ );
+}
diff --git a/src/components/TopRepos.tsx b/src/components/TopRepos.tsx
index a7b96ea7..4f7a5915 100644
--- a/src/components/TopRepos.tsx
+++ b/src/components/TopRepos.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import { useAccount } from "@/components/AccountContext";
import type { RepoHealthScore } from "@/types/repo-health";
+import RepoHealthPanel from "@/components/RepoHealthPanel";
interface RepoLanguage {
name: string;
@@ -84,7 +85,7 @@ export default function TopRepos() {
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
const [pinnedRepos, setPinnedRepos] = useState([]);
const [pinError, setPinError] = useState(null);
-
+ const [activeHealthRepo, setActiveHealthRepo] = useState(null);
useEffect(() => {
fetch("/api/user/settings")
.then((r) => r.json())
@@ -326,12 +327,15 @@ export default function TopRepos() {
{healthLoading ? (
) : health ? (
- setActiveHealthRepo(repo.name)}
+ className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold cursor-pointer ${badgeClass}`}
title={badgeTitle}
+ aria-label={`View health breakdown for ${shortName}`}
>
{health.score}
-
+
) : (
)}
+ {activeHealthRepo && healthScores[activeHealthRepo] && (
+ setActiveHealthRepo(null)}
+ />
+ )}
);
}