From 858ddfc8de6b61d2aea99d633f36570dfa7e8857 Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 10:45:00 -0400 Subject: [PATCH 1/7] feat(ui): creates shared RepoGroupHeader with info tint Extracts RepoGroupHeader component with bg-info/20 tint, oklch text color via CSS light-dark(), and slot-based API (badges, trailing, children). Adds @utility repo-header-text with adaptive oklch colors. Fixes animate-reorder-highlight fill-mode (forwards to none) to prevent bg-info/20 override after animation ends. Includes 16 unit tests for RepoGroupHeader component. --- src/app/components/shared/RepoGroupHeader.tsx | 39 ++++++ src/app/index.css | 6 +- .../shared/RepoGroupHeader.test.tsx | 131 ++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/app/components/shared/RepoGroupHeader.tsx create mode 100644 tests/components/shared/RepoGroupHeader.test.tsx diff --git a/src/app/components/shared/RepoGroupHeader.tsx b/src/app/components/shared/RepoGroupHeader.tsx new file mode 100644 index 00000000..563db5fc --- /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; + children?: JSX.Element; + trailing?: JSX.Element; + badges?: JSX.Element; +} + +export default function RepoGroupHeader(props: RepoGroupHeaderProps) { + return ( +
+ + {props.trailing} +
+ ); +} diff --git a/src/app/index.css b/src/app/index.css index 1823df6b..c64efac8 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.85 0.18 250)); } /* ── Notification animations ──────────────────────────────────────────────── */ diff --git a/tests/components/shared/RepoGroupHeader.test.tsx b/tests/components/shared/RepoGroupHeader.test.tsx new file mode 100644 index 00000000..885122b3 --- /dev/null +++ b/tests/components/shared/RepoGroupHeader.test.tsx @@ -0,0 +1,131 @@ +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")); + expect(onToggle).toHaveBeenCalledTimes(1); + }); + + it("renders children when collapsed", () => { + const { getByText } = render(() => ( + + collapsed content + + )); + expect(getByText("collapsed content")).toBeTruthy(); + }); + + it("hides children 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").getAttribute("aria-expanded")).toBe("true"); + }); + + it("sets aria-expanded=false when collapsed", () => { + const { getByRole } = render(() => ( + + )); + expect(getByRole("button").getAttribute("aria-expanded")).toBe("false"); + }); +}); From 3cbe5d86919e698bb338d0384388074ba2a1d116 Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 10:45:44 -0400 Subject: [PATCH 2/7] refactor(issues): migrates IssuesTab to shared RepoGroupHeader Replaces inline header markup with RepoGroupHeader component. Removes ChevronIcon and formatStarCount imports. Retains Tooltip for Monitoring all badge and Dep Dashboard toggle. --- src/app/components/dashboard/IssuesTab.tsx | 67 +++++++++---------- tests/components/dashboard/IssuesTab.test.tsx | 2 +- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/app/components/dashboard/IssuesTab.tsx b/src/app/components/dashboard/IssuesTab.tsx index bef78a88..f178c1be 100644 --- a/src/app/components/dashboard/IssuesTab.tsx +++ b/src/app/components/dashboard/IssuesTab.tsx @@ -10,9 +10,9 @@ import { scopeFilterGroup, type FilterChipGroupDef } from "../shared/filterTypes import FilterToolbar from "../shared/FilterToolbar"; import RoleBadge from "../shared/RoleBadge"; import SkeletonRows from "../shared/SkeletonRows"; -import ChevronIcon from "../shared/ChevronIcon"; import ExpandCollapseButtons from "../shared/ExpandCollapseButtons"; -import { deriveInvolvementRoles, formatStarCount } from "../../lib/format"; +import { deriveInvolvementRoles } from "../../lib/format"; +import RepoGroupHeader from "../shared/RepoGroupHeader"; import { groupByRepo, computePageLayout, slicePageGroups, orderRepoGroups, isUserInvolved } from "../../lib/grouping"; import { createReorderHighlight } from "../../lib/reorderHighlight"; import RepoLockControls from "../shared/RepoLockControls"; @@ -351,44 +351,41 @@ export default function IssuesTab(props: IssuesTabProps) { return (
-
- - - -
+ } + trailing={ + <> + + + + } + > + + {repoGroup.items.length} {repoGroup.items.length === 1 ? "issue" : "issues"} + + {([role, count]) => ( + + {role} ×{count} + + )} + + +
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", () => { From f051569723f06546ae589977c9f5044e9ca02302 Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 10:46:26 -0400 Subject: [PATCH 3/7] refactor(prs): migrates PullRequestsTab to shared RepoGroupHeader Replaces inline header markup with RepoGroupHeader component. Removes ChevronIcon and formatStarCount imports. Retains Tooltip for item rows and Monitoring all badge. Peek update div stays as sibling outside the component. --- .../components/dashboard/PullRequestsTab.tsx | 143 +++++++++--------- .../dashboard/PullRequestsTab.test.tsx | 2 +- 2 files changed, 71 insertions(+), 74 deletions(-) diff --git a/src/app/components/dashboard/PullRequestsTab.tsx b/src/app/components/dashboard/PullRequestsTab.tsx index 105721f1..49424c13 100644 --- a/src/app/components/dashboard/PullRequestsTab.tsx +++ b/src/app/components/dashboard/PullRequestsTab.tsx @@ -2,7 +2,7 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; import { config, type TrackedUser } from "../../stores/config"; import { viewState, ignoreItem, unignoreItem, setTabFilter, resetAllTabFilters, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, trackItem, untrackItem, type PullRequestFilterField } from "../../stores/view"; import type { PullRequest, RepoRef } from "../../services/api"; -import { deriveInvolvementRoles, prSizeCategory, formatStarCount } from "../../lib/format"; +import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format"; import { isSafeGitHubUrl } from "../../lib/url"; import ExpandCollapseButtons from "../shared/ExpandCollapseButtons"; import ItemRow from "./ItemRow"; @@ -16,7 +16,7 @@ import ReviewBadge from "../shared/ReviewBadge"; import SizeBadge from "../shared/SizeBadge"; import RoleBadge from "../shared/RoleBadge"; import SkeletonRows from "../shared/SkeletonRows"; -import ChevronIcon from "../shared/ChevronIcon"; +import RepoGroupHeader from "../shared/RepoGroupHeader"; import { groupByRepo, computePageLayout, slicePageGroups, orderRepoGroups, isUserInvolved } from "../../lib/grouping"; import { createReorderHighlight } from "../../lib/reorderHighlight"; import { createFlashDetection } from "../../lib/flashDetection"; @@ -458,86 +458,83 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { return (
-
- - - -
+ 0}> + + {`Needs review ×${summaryMeta().reviews.REVIEW_REQUIRED}`} + + + + {([role, count]) => ( + + {`${role} ×${count}`} + + )} + + + {(peek) => (
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", () => { From 767f742d778464fef868e3878c1f28347c53995e Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 10:47:11 -0400 Subject: [PATCH 4/7] refactor(actions): migrates ActionsTab to shared RepoGroupHeader Replaces inline header markup with RepoGroupHeader component. Removes ChevronIcon import. Omits starCount and badges props (ActionsTab has neither). Peek update div stays as sibling. --- src/app/components/dashboard/ActionsTab.tsx | 70 ++++++++++----------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/src/app/components/dashboard/ActionsTab.tsx b/src/app/components/dashboard/ActionsTab.tsx index 54aeac56..2c12089c 100644 --- a/src/app/components/dashboard/ActionsTab.tsx +++ b/src/app/components/dashboard/ActionsTab.tsx @@ -7,7 +7,7 @@ import IgnoreBadge from "./IgnoreBadge"; import SkeletonRows from "../shared/SkeletonRows"; import type { FilterChipGroupDef } from "../shared/filterTypes"; import FilterToolbar from "../shared/FilterToolbar"; -import ChevronIcon from "../shared/ChevronIcon"; +import RepoGroupHeader from "../shared/RepoGroupHeader"; import ExpandCollapseButtons from "../shared/ExpandCollapseButtons"; import RepoLockControls from "../shared/RepoLockControls"; import RepoGitHubLink from "../shared/RepoGitHubLink"; @@ -294,42 +294,40 @@ export default function ActionsTab(props: ActionsTabProps) { return (
- {/* Repo header */} -
- - - -
+ + {(peek) => (
From 276c855d523ea7969f59db125c3e7c778f4ffda1 Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 11:08:58 -0400 Subject: [PATCH 5/7] style(ui): add compact:text-sm to RepoGroupHeader for density parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores compact-mode font size (text-sm/14px) while keeping text-base/16px for normal density. Without this, compact repo headers were 36px vs old 32px — a 12.5% height regression. --- src/app/components/shared/RepoGroupHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/shared/RepoGroupHeader.tsx b/src/app/components/shared/RepoGroupHeader.tsx index 563db5fc..36fff70f 100644 --- a/src/app/components/shared/RepoGroupHeader.tsx +++ b/src/app/components/shared/RepoGroupHeader.tsx @@ -19,7 +19,7 @@ export default function RepoGroupHeader(props: RepoGroupHeaderProps) { {props.trailing} 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/shared/RepoGroupHeader.test.tsx b/tests/components/shared/RepoGroupHeader.test.tsx index 885122b3..a8d110dc 100644 --- a/tests/components/shared/RepoGroupHeader.test.tsx +++ b/tests/components/shared/RepoGroupHeader.test.tsx @@ -63,24 +63,21 @@ describe("RepoGroupHeader", () => { const { getByRole } = render(() => ( )); - fireEvent.click(getByRole("button")); + fireEvent.click(getByRole("button", { name: /owner\/repo/ })); expect(onToggle).toHaveBeenCalledTimes(1); + expect(onToggle).toHaveBeenCalledWith(); }); - it("renders children when collapsed", () => { + it("renders collapsedSummary when collapsed", () => { const { getByText } = render(() => ( - - collapsed content - + collapsed content} /> )); expect(getByText("collapsed content")).toBeTruthy(); }); - it("hides children when expanded", () => { + it("hides collapsedSummary when expanded", () => { const { queryByText } = render(() => ( - - collapsed content - + collapsed content} /> )); expect(queryByText("collapsed content")).toBeNull(); }); @@ -119,13 +116,18 @@ describe("RepoGroupHeader", () => { const { getByRole } = render(() => ( )); - expect(getByRole("button").getAttribute("aria-expanded")).toBe("true"); + expect(getByRole("button", { name: /owner\/repo/ }).getAttribute("aria-expanded")).toBe("true"); }); it("sets aria-expanded=false when collapsed", () => { const { getByRole } = render(() => ( )); - expect(getByRole("button").getAttribute("aria-expanded")).toBe("false"); + 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"); }); });