diff --git a/src/app/components/shared/RepoGroupHeader.tsx b/src/app/components/shared/RepoGroupHeader.tsx
new file mode 100644
index 00000000..330278ce
--- /dev/null
+++ b/src/app/components/shared/RepoGroupHeader.tsx
@@ -0,0 +1,39 @@
+import { Show, type JSX } from "solid-js";
+import ChevronIcon from "./ChevronIcon";
+import { formatStarCount } from "../../lib/format";
+
+interface RepoGroupHeaderProps {
+ repoFullName: string;
+ starCount?: number | null;
+ isExpanded: boolean;
+ isHighlighted?: boolean;
+ onToggle: () => void;
+ collapsedSummary?: JSX.Element;
+ trailing?: JSX.Element;
+ badges?: JSX.Element;
+}
+
+export default function RepoGroupHeader(props: RepoGroupHeaderProps) {
+ return (
+
+ );
+}
diff --git a/src/app/index.css b/src/app/index.css
index 1823df6b..09a9d74b 100644
--- a/src/app/index.css
+++ b/src/app/index.css
@@ -51,7 +51,11 @@
}
@utility animate-reorder-highlight {
- animation: reorder-highlight 1.5s ease-out forwards;
+ animation: reorder-highlight 1.5s ease-out;
+}
+
+@utility repo-header-text {
+ color: light-dark(oklch(0.45 0.15 250), oklch(0.70 0.15 250));
}
/* ── Notification animations ──────────────────────────────────────────────── */
diff --git a/tests/components/dashboard/ActionsTab.test.tsx b/tests/components/dashboard/ActionsTab.test.tsx
index a0a036ef..88768042 100644
--- a/tests/components/dashboard/ActionsTab.test.tsx
+++ b/tests/components/dashboard/ActionsTab.test.tsx
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@solidjs/testing-library";
+import userEvent from "@testing-library/user-event";
import { makeWorkflowRun } from "../../helpers/index";
// ── localStorage mock ─────────────────────────────────────────────────────────
@@ -104,3 +105,44 @@ describe("ActionsTab — empty-repo state preservation", () => {
expect(viewState.lockedRepos).toEqual([]);
});
});
+
+// ── ActionsTab — RepoGroupHeader integration ──────────────────────────────────
+
+describe("ActionsTab — RepoGroupHeader rendering", () => {
+ it("renders the repo name in the group header when workflow runs are present", () => {
+ const runs = [makeWorkflowRun({ repoFullName: "owner/my-repo" })];
+ render(() => (
+
+ ));
+ // The header toggle button has aria-expanded, distinguishing it from pin/unpin buttons
+ const headerBtn = screen.getAllByRole("button", { name: /owner\/my-repo/i })
+ .find(btn => btn.hasAttribute("aria-expanded"));
+ expect(headerBtn).toBeTruthy();
+ });
+
+ it("toggles repo group expanded state when header button is clicked", async () => {
+ const user = userEvent.setup();
+ const runs = [makeWorkflowRun({ repoFullName: "owner/repo" })];
+
+ render(() => (
+
+ ));
+
+ const headerBtn = screen.getAllByRole("button", { name: /owner\/repo/i })
+ .find(btn => btn.hasAttribute("aria-expanded"))!;
+
+ // Initially collapsed
+ expect(headerBtn.getAttribute("aria-expanded")).toBe("false");
+ expect(viewState.expandedRepos.actions["owner/repo"]).toBeFalsy();
+
+ // Click to expand
+ await user.click(headerBtn);
+ expect(headerBtn.getAttribute("aria-expanded")).toBe("true");
+ expect(viewState.expandedRepos.actions["owner/repo"]).toBe(true);
+
+ // Click again to collapse
+ await user.click(headerBtn);
+ expect(headerBtn.getAttribute("aria-expanded")).toBe("false");
+ expect(viewState.expandedRepos.actions["owner/repo"]).toBeFalsy();
+ });
+});
diff --git a/tests/components/dashboard/IssuesTab.test.tsx b/tests/components/dashboard/IssuesTab.test.tsx
index da4724b0..c4d168e8 100644
--- a/tests/components/dashboard/IssuesTab.test.tsx
+++ b/tests/components/dashboard/IssuesTab.test.tsx
@@ -511,7 +511,7 @@ describe("IssuesTab — star count in repo headers", () => {
/>
));
- screen.getByText("★ 1.2k");
+ screen.getByLabelText("1234 stars");
});
it("does not show star display when starCount is undefined", () => {
diff --git a/tests/components/dashboard/PullRequestsTab.test.tsx b/tests/components/dashboard/PullRequestsTab.test.tsx
index 1a7d8df8..373e94c3 100644
--- a/tests/components/dashboard/PullRequestsTab.test.tsx
+++ b/tests/components/dashboard/PullRequestsTab.test.tsx
@@ -461,7 +461,7 @@ describe("PullRequestsTab — star count in repo headers", () => {
/>
));
- screen.getByText("★ 1.2k");
+ screen.getByLabelText("1234 stars");
});
it("does not show star display when starCount is undefined", () => {
diff --git a/tests/components/shared/RepoGroupHeader.test.tsx b/tests/components/shared/RepoGroupHeader.test.tsx
new file mode 100644
index 00000000..a8d110dc
--- /dev/null
+++ b/tests/components/shared/RepoGroupHeader.test.tsx
@@ -0,0 +1,133 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, fireEvent } from "@solidjs/testing-library";
+import RepoGroupHeader from "../../../src/app/components/shared/RepoGroupHeader";
+
+describe("RepoGroupHeader", () => {
+ const defaultProps = {
+ repoFullName: "owner/repo",
+ isExpanded: true,
+ onToggle: vi.fn(),
+ };
+
+ it("renders repo name", () => {
+ const { getByText } = render(() =>
);
+ expect(getByText("owner/repo")).toBeTruthy();
+ });
+
+ it("renders star count when provided", () => {
+ const { getByLabelText } = render(() => (
+
+ ));
+ expect(getByLabelText("1234 stars")).toBeTruthy();
+ });
+
+ it("hides star count when null", () => {
+ const { queryByLabelText } = render(() => (
+
+ ));
+ expect(queryByLabelText(/stars/)).toBeNull();
+ });
+
+ it("hides star count when undefined", () => {
+ const { queryByLabelText } = render(() => (
+
+ ));
+ expect(queryByLabelText(/stars/)).toBeNull();
+ });
+
+ it("hides star count when zero", () => {
+ const { queryByLabelText } = render(() => (
+
+ ));
+ expect(queryByLabelText(/stars/)).toBeNull();
+ });
+
+ it("renders chevron rotated when collapsed", () => {
+ const { container } = render(() => (
+
+ ));
+ const svg = container.querySelector("svg");
+ expect(svg?.getAttribute("class")).toContain("-rotate-90");
+ });
+
+ it("renders chevron not rotated when expanded", () => {
+ const { container } = render(() => (
+
+ ));
+ const svg = container.querySelector("svg");
+ expect(svg?.getAttribute("class")).not.toContain("-rotate-90");
+ });
+
+ it("calls onToggle on button click", () => {
+ const onToggle = vi.fn();
+ const { getByRole } = render(() => (
+
+ ));
+ fireEvent.click(getByRole("button", { name: /owner\/repo/ }));
+ expect(onToggle).toHaveBeenCalledTimes(1);
+ expect(onToggle).toHaveBeenCalledWith();
+ });
+
+ it("renders collapsedSummary when collapsed", () => {
+ const { getByText } = render(() => (
+
collapsed content} />
+ ));
+ expect(getByText("collapsed content")).toBeTruthy();
+ });
+
+ it("hides collapsedSummary when expanded", () => {
+ const { queryByText } = render(() => (
+ collapsed content} />
+ ));
+ expect(queryByText("collapsed content")).toBeNull();
+ });
+
+ it("renders trailing slot", () => {
+ const { getByText } = render(() => (
+ trailing} />
+ ));
+ expect(getByText("trailing")).toBeTruthy();
+ });
+
+ it("renders badges slot", () => {
+ const { getByText } = render(() => (
+ badge} />
+ ));
+ expect(getByText("badge")).toBeTruthy();
+ });
+
+ it("applies animate-reorder-highlight when isHighlighted", () => {
+ const { container } = render(() => (
+
+ ));
+ const outer = container.firstElementChild as HTMLElement;
+ expect(outer.className).toContain("animate-reorder-highlight");
+ });
+
+ it("does not apply animate-reorder-highlight when not highlighted", () => {
+ const { container } = render(() => (
+
+ ));
+ const outer = container.firstElementChild as HTMLElement;
+ expect(outer.className).not.toContain("animate-reorder-highlight");
+ });
+
+ it("sets aria-expanded correctly", () => {
+ const { getByRole } = render(() => (
+
+ ));
+ expect(getByRole("button", { name: /owner\/repo/ }).getAttribute("aria-expanded")).toBe("true");
+ });
+
+ it("sets aria-expanded=false when collapsed", () => {
+ const { getByRole } = render(() => (
+
+ ));
+ expect(getByRole("button", { name: /owner\/repo/ }).getAttribute("aria-expanded")).toBe("false");
+ });
+
+ it("applies repo-header-text class to the button", () => {
+ const { getByRole } = render(() => );
+ expect(getByRole("button", { name: /owner\/repo/ }).className).toContain("repo-header-text");
+ });
+});