authorgravatar for 136640686+MatthewFeroz@users.noreply.github.comMatthew Feroz <136640686+MatthewFeroz@users.noreply.github.com> 2026-08-30 22:04:43-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-31 02:04:43+00:00
logcefec32d6fc5d14f03e110ebdde534bdbcc9b62b
treeedf9a11d95b522a942595f34e33a5e38376f1c9c
parent352710d497cc640553e3e18e23fb5a5f3f890466
signature Signed by PGP key B5690EEEBB952194

fix(web): prevent pull request metadata overlap (#8790)


3 files changed, 59 insertions(+), 29 deletions(-)

apps/web/src/components/pullRequest/PullRequestRow.tsx+38-16
......@@ -1,3 +1,4 @@
1import { SearchIcon } from "lucide-react";
12import { memo } from "react";
23
34import { cn } from "~/lib/utils";
......@@ -45,7 +46,7 @@ function PullRequestRowImpl({
4546 aria-current={selected ? "true" : undefined}
4647 onClick={() => onSelect(entry)}
4748 className={cn(
48 "grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
49 "grid w-full grid-cols-[auto_minmax(0,1fr)] items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
4950 // Offscreen rows are skipped for style, layout and paint: a long list costs what the
5051 // viewport shows, not what the pages have loaded. The intrinsic size keeps the
5152 // scrollbar honest while a row is skipped.
......@@ -59,9 +60,30 @@ function PullRequestRowImpl({
5960 mergeability={entry.mergeability}
6061 baseBranch={entry.baseBranch}
6162 />
62 <span className="min-w-0">
63 <span className="block truncate text-sm font-medium text-foreground">{entry.title}</span>
64 <PullRequestMetaLine className="mt-0.5 text-xs text-muted-foreground/70">
63 <span className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5">
64 <span className="col-start-1 row-start-1 block truncate text-sm font-medium text-foreground">
65 {entry.title}
66 </span>
67 <span className="col-start-2 row-start-1 justify-self-end whitespace-nowrap text-xs text-muted-foreground/70 tabular-nums">
68 {formatRelativeTimeLabel(entry.updatedAt)}
69 </span>
70 <PullRequestMetaLine className="@container/pr-row-meta col-start-1 row-start-2 overflow-hidden text-xs text-muted-foreground/70">
71 {matchedElsewhere ? (
72 <Tooltip>
73 <TooltipTrigger
74 render={
75 <span className="flex min-w-6 items-center gap-1 overflow-hidden rounded-full border border-border/60 px-1 text-[10px]" />
76 }
77 >
78 <span className="sr-only">matched in the description</span>
79 <SearchIcon aria-hidden className="size-3 shrink-0" />
80 <span aria-hidden className="hidden truncate @xs/pr-row-meta:block">
81 matched in the description
82 </span>
83 </TooltipTrigger>
84 <TooltipPopup side="top">Matched in the description</TooltipPopup>
85 </Tooltip>
86 ) : null}
6587 <span className="flex shrink-0 items-center gap-1">
6688 {showProvider ? (
6789 <Tooltip>
......@@ -89,15 +111,19 @@ function PullRequestRowImpl({
89111 </span>
90112 {showProjectTitle ? <span className="truncate">{entry.repository}</span> : null}
91113 {environmentLabel ? (
92 <span className="max-w-32 shrink-0 truncate">{environmentLabel}</span>
114 <span className="min-w-0 max-w-32 truncate">{environmentLabel}</span>
93115 ) : null}
94 <PullRequestActorLabel actor={entry.author} className="max-w-40 shrink-0" />
116 <PullRequestActorLabel
117 actor={entry.author}
118 className="min-w-4 max-w-40"
119 labelClassName="sr-only @xs/pr-row-meta:not-sr-only @xs/pr-row-meta:truncate"
120 />
95121 {/* Only a verdict somebody has actually given: "review required" is the absence of
96122 one, and saying so on every unreviewed row would say nothing. */}
97123 {entry.reviewDecision === "approved" || entry.reviewDecision === "changes-requested" ? (
98124 <span
99125 className={cn(
100 "shrink-0",
126 "min-w-0 truncate",
101127 entry.reviewDecision === "approved"
102128 ? "text-emerald-600/90 dark:text-emerald-400/80"
103129 : "text-amber-600/90 dark:text-amber-400/80",
......@@ -117,16 +143,12 @@ function PullRequestRowImpl({
117143 }}
118144 />
119145 )}
120 {matchedElsewhere ? (
121 <span className="shrink-0 rounded-full border border-border/60 px-1.5 text-[10px]">
122 matched in the description
123 </span>
124 ) : null}
125146 </PullRequestMetaLine>
126 </span>
127 <span className="flex shrink-0 flex-col items-end gap-0.5 text-xs text-muted-foreground/70 tabular-nums">
128 <span>{formatRelativeTimeLabel(entry.updatedAt)}</span>
129 <PullRequestDiffStat additions={entry.additions} deletions={entry.deletions} />
147 <PullRequestDiffStat
148 additions={entry.additions}
149 deletions={entry.deletions}
150 className="col-start-2 row-start-2 justify-self-end text-xs"
151 />
130152 </span>
131153 </button>
132154 );
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+3-1
......@@ -339,17 +339,19 @@ export function PullRequestActorAvatar({
339339export function PullRequestActorLabel({
340340 actor,
341341 className,
342 labelClassName,
342343 tooltip = true,
343344}: {
344345 actor: PullRequestActor | null;
345346 className?: string;
347 labelClassName?: string;
346348 tooltip?: boolean;
347349}) {
348350 const login = actor?.login ?? "ghost";
349351 const label = (
350352 <>
351353 <PullRequestActorAvatar actor={actor} />
352 <span className="truncate">{login}</span>
354 <span className={cn("truncate", labelClassName)}>{login}</span>
353355 </>
354356 );
355357 if (!tooltip) {
apps/web/src/routes/_chat.pull-requests.tsx+18-12
......@@ -1628,11 +1628,13 @@ function CompactFilterMenu<Value extends string>({
16281628 value,
16291629 options,
16301630 onChange,
1631 className,
16311632}: {
16321633 label: string;
16331634 value: Value;
16341635 options: ReadonlyArray<PullRequestFilterOption<Value>>;
16351636 onChange: (value: Value) => void;
1637 className?: string;
16361638}) {
16371639 const current = options.find((option) => option.value === value) ?? options[0];
16381640 if (!current) return null;
......@@ -1640,10 +1642,13 @@ function CompactFilterMenu<Value extends string>({
16401642 <Menu>
16411643 <MenuTrigger
16421644 aria-label={label}
1643 className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md px-1.5 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
1645 className={cn(
1646 "inline-flex h-7 min-w-0 items-center gap-1 rounded-md px-1.5 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground",
1647 className,
1648 )}
16441649 >
1645 {current.label}
1646 <ChevronDownIcon aria-hidden className="size-3 text-muted-foreground/70" />
1650 <span className="truncate">{current.label}</span>
1651 <ChevronDownIcon aria-hidden className="size-3 shrink-0 text-muted-foreground/70" />
16471652 </MenuTrigger>
16481653 <MenuPopup align="start" side="bottom" className="min-w-40">
16491654 <MenuRadioGroup value={value} onValueChange={(next) => onChange(next as Value)}>
......@@ -1721,7 +1726,7 @@ function ExpandableSearch({
17211726 return (
17221727 <div
17231728 ref={containerRef}
1724 className="w-56 shrink-0"
1729 className="w-56 min-w-24 shrink"
17251730 onFocus={() => onFocusWithin?.(true)}
17261731 onBlur={() => {
17271732 onFocusWithin?.(false);
......@@ -1808,6 +1813,7 @@ function PullRequestsColumn({
18081813 const inFlowSearchRef = useRef<HTMLDivElement | null>(null);
18091814 const [searchOpen, setSearchOpen] = useState(false);
18101815 const [searchFocusToken, setSearchFocusToken] = useState(0);
1816 const searchExpanded = searchOpen || searchValue.length > 0;
18111817 // Mod+F belongs to this page's own search: the desktop shell binds no find-in-page, so the
18121818 // shortcut would otherwise do nothing. Condensed, it unfolds the topbar search; at the top,
18131819 // it focuses the in-flow bar and selects the query the way a find field would.
......@@ -1860,20 +1866,20 @@ function PullRequestsColumn({
18601866 >
18611867 {titlebarControls}
18621868 {condensed ? (
1863 <WorkspaceBreadcrumb ariaLabel="Pull request scope">
1864 {/* The page name remains the foreground anchor in both states; the live filters are
1865 its compact scope, grouped as the second crumb rather than pretending each menu
1866 is a separate page in the hierarchy. */}
1867 <WorkspaceBreadcrumbItem current>
1869 <WorkspaceBreadcrumb ariaLabel="Pull request scope" className="overflow-hidden">
1870 {/* An expanded search owns the scarce horizontal space. The page title stays
1871 available to readers while the live filters remain available in both states. */}
1872 <WorkspaceBreadcrumbItem current className={cn(searchExpanded && "sr-only")}>
18681873 <h1 className="truncate">Pull Requests</h1>
18691874 </WorkspaceBreadcrumbItem>
1870 <WorkspaceBreadcrumbSeparator />
1871 <WorkspaceBreadcrumbItem className="gap-1.5 overflow-hidden">
1875 {searchExpanded ? null : <WorkspaceBreadcrumbSeparator />}
1876 <WorkspaceBreadcrumbItem className="shrink gap-1.5">
18721877 <CompactFilterMenu
18731878 label="Filter by state"
18741879 value={state}
18751880 options={STATE_TABS}
18761881 onChange={onState}
1882 className="shrink-0"
18771883 />
18781884 <CompactFilterMenu
18791885 label="Filter by involvement"
......@@ -1900,7 +1906,7 @@ function PullRequestsColumn({
19001906 )}
19011907 <div className="min-w-0 flex-1" />
19021908 {condensed ? (
1903 <div className="flex shrink-0 items-center gap-1.5">
1909 <div className="flex shrink items-center gap-1.5">
19041910 <ExpandableSearch
19051911 searchInput={searchInput}
19061912 searchValue={searchValue}