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 @@...@@ -1,3 +1,4 @@
1import { SearchIcon } from "lucide-react";
1import { memo } from "react";2import { memo } from "react";
23
3import { cn } from "~/lib/utils";4import { cn } from "~/lib/utils";
...@@ -45,7 +46,7 @@ function PullRequestRowImpl({...@@ -45,7 +46,7 @@ function PullRequestRowImpl({
45 aria-current={selected ? "true" : undefined}46 aria-current={selected ? "true" : undefined}
46 onClick={() => onSelect(entry)}47 onClick={() => onSelect(entry)}
47 className={cn(48 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",
49 // Offscreen rows are skipped for style, layout and paint: a long list costs what the50 // Offscreen rows are skipped for style, layout and paint: a long list costs what the
50 // viewport shows, not what the pages have loaded. The intrinsic size keeps the51 // viewport shows, not what the pages have loaded. The intrinsic size keeps the
51 // scrollbar honest while a row is skipped.52 // scrollbar honest while a row is skipped.
...@@ -59,9 +60,30 @@ function PullRequestRowImpl({...@@ -59,9 +60,30 @@ function PullRequestRowImpl({
59 mergeability={entry.mergeability}60 mergeability={entry.mergeability}
60 baseBranch={entry.baseBranch}61 baseBranch={entry.baseBranch}
61 />62 />
62 <span className="min-w-0">63 <span className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-0.5">
63 <span className="block truncate text-sm font-medium text-foreground">{entry.title}</span>64 <span className="col-start-1 row-start-1 block truncate text-sm font-medium text-foreground">
64 <PullRequestMetaLine className="mt-0.5 text-xs text-muted-foreground/70">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}
65 <span className="flex shrink-0 items-center gap-1">87 <span className="flex shrink-0 items-center gap-1">
66 {showProvider ? (88 {showProvider ? (
67 <Tooltip>89 <Tooltip>
...@@ -89,15 +111,19 @@ function PullRequestRowImpl({...@@ -89,15 +111,19 @@ function PullRequestRowImpl({
89 </span>111 </span>
90 {showProjectTitle ? <span className="truncate">{entry.repository}</span> : null}112 {showProjectTitle ? <span className="truncate">{entry.repository}</span> : null}
91 {environmentLabel ? (113 {environmentLabel ? (
92 <span className="max-w-32 shrink-0 truncate">{environmentLabel}</span>114 <span className="min-w-0 max-w-32 truncate">{environmentLabel}</span>
93 ) : null}115 ) : 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 />
95 {/* Only a verdict somebody has actually given: "review required" is the absence of121 {/* Only a verdict somebody has actually given: "review required" is the absence of
96 one, and saying so on every unreviewed row would say nothing. */}122 one, and saying so on every unreviewed row would say nothing. */}
97 {entry.reviewDecision === "approved" || entry.reviewDecision === "changes-requested" ? (123 {entry.reviewDecision === "approved" || entry.reviewDecision === "changes-requested" ? (
98 <span124 <span
99 className={cn(125 className={cn(
100 "shrink-0",126 "min-w-0 truncate",
101 entry.reviewDecision === "approved"127 entry.reviewDecision === "approved"
102 ? "text-emerald-600/90 dark:text-emerald-400/80"128 ? "text-emerald-600/90 dark:text-emerald-400/80"
103 : "text-amber-600/90 dark:text-amber-400/80",129 : "text-amber-600/90 dark:text-amber-400/80",
...@@ -117,16 +143,12 @@ function PullRequestRowImpl({...@@ -117,16 +143,12 @@ function PullRequestRowImpl({
117 }}143 }}
118 />144 />
119 )}145 )}
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}
125 </PullRequestMetaLine>146 </PullRequestMetaLine>
126 </span>147 <PullRequestDiffStat
127 <span className="flex shrink-0 flex-col items-end gap-0.5 text-xs text-muted-foreground/70 tabular-nums">148 additions={entry.additions}
128 <span>{formatRelativeTimeLabel(entry.updatedAt)}</span>149 deletions={entry.deletions}
129 <PullRequestDiffStat additions={entry.additions} deletions={entry.deletions} />150 className="col-start-2 row-start-2 justify-self-end text-xs"
151 />
130 </span>152 </span>
131 </button>153 </button>
132 );154 );
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+3-1
...@@ -339,17 +339,19 @@ export function PullRequestActorAvatar({...@@ -339,17 +339,19 @@ export function PullRequestActorAvatar({
339export function PullRequestActorLabel({339export function PullRequestActorLabel({
340 actor,340 actor,
341 className,341 className,
342 labelClassName,
342 tooltip = true,343 tooltip = true,
343}: {344}: {
344 actor: PullRequestActor | null;345 actor: PullRequestActor | null;
345 className?: string;346 className?: string;
347 labelClassName?: string;
346 tooltip?: boolean;348 tooltip?: boolean;
347}) {349}) {
348 const login = actor?.login ?? "ghost";350 const login = actor?.login ?? "ghost";
349 const label = (351 const label = (
350 <>352 <>
351 <PullRequestActorAvatar actor={actor} />353 <PullRequestActorAvatar actor={actor} />
352 <span className="truncate">{login}</span>354 <span className={cn("truncate", labelClassName)}>{login}</span>
353 </>355 </>
354 );356 );
355 if (!tooltip) {357 if (!tooltip) {
apps/web/src/routes/_chat.pull-requests.tsx+18-12
...@@ -1628,11 +1628,13 @@ function CompactFilterMenu<Value extends string>({...@@ -1628,11 +1628,13 @@ function CompactFilterMenu<Value extends string>({
1628 value,1628 value,
1629 options,1629 options,
1630 onChange,1630 onChange,
1631 className,
1631}: {1632}: {
1632 label: string;1633 label: string;
1633 value: Value;1634 value: Value;
1634 options: ReadonlyArray<PullRequestFilterOption<Value>>;1635 options: ReadonlyArray<PullRequestFilterOption<Value>>;
1635 onChange: (value: Value) => void;1636 onChange: (value: Value) => void;
1637 className?: string;
1636}) {1638}) {
1637 const current = options.find((option) => option.value === value) ?? options[0];1639 const current = options.find((option) => option.value === value) ?? options[0];
1638 if (!current) return null;1640 if (!current) return null;
...@@ -1640,10 +1642,13 @@ function CompactFilterMenu<Value extends string>({...@@ -1640,10 +1642,13 @@ function CompactFilterMenu<Value extends string>({
1640 <Menu>1642 <Menu>
1641 <MenuTrigger1643 <MenuTrigger
1642 aria-label={label}1644 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 )}
1644 >1649 >
1645 {current.label}1650 <span className="truncate">{current.label}</span>
1646 <ChevronDownIcon aria-hidden className="size-3 text-muted-foreground/70" />1651 <ChevronDownIcon aria-hidden className="size-3 shrink-0 text-muted-foreground/70" />
1647 </MenuTrigger>1652 </MenuTrigger>
1648 <MenuPopup align="start" side="bottom" className="min-w-40">1653 <MenuPopup align="start" side="bottom" className="min-w-40">
1649 <MenuRadioGroup value={value} onValueChange={(next) => onChange(next as Value)}>1654 <MenuRadioGroup value={value} onValueChange={(next) => onChange(next as Value)}>
...@@ -1721,7 +1726,7 @@ function ExpandableSearch({...@@ -1721,7 +1726,7 @@ function ExpandableSearch({
1721 return (1726 return (
1722 <div1727 <div
1723 ref={containerRef}1728 ref={containerRef}
1724 className="w-56 shrink-0"1729 className="w-56 min-w-24 shrink"
1725 onFocus={() => onFocusWithin?.(true)}1730 onFocus={() => onFocusWithin?.(true)}
1726 onBlur={() => {1731 onBlur={() => {
1727 onFocusWithin?.(false);1732 onFocusWithin?.(false);
...@@ -1808,6 +1813,7 @@ function PullRequestsColumn({...@@ -1808,6 +1813,7 @@ function PullRequestsColumn({
1808 const inFlowSearchRef = useRef<HTMLDivElement | null>(null);1813 const inFlowSearchRef = useRef<HTMLDivElement | null>(null);
1809 const [searchOpen, setSearchOpen] = useState(false);1814 const [searchOpen, setSearchOpen] = useState(false);
1810 const [searchFocusToken, setSearchFocusToken] = useState(0);1815 const [searchFocusToken, setSearchFocusToken] = useState(0);
1816 const searchExpanded = searchOpen || searchValue.length > 0;
1811 // Mod+F belongs to this page's own search: the desktop shell binds no find-in-page, so the1817 // Mod+F belongs to this page's own search: the desktop shell binds no find-in-page, so the
1812 // shortcut would otherwise do nothing. Condensed, it unfolds the topbar search; at the top,1818 // shortcut would otherwise do nothing. Condensed, it unfolds the topbar search; at the top,
1813 // it focuses the in-flow bar and selects the query the way a find field would.1819 // it focuses the in-flow bar and selects the query the way a find field would.
...@@ -1860,20 +1866,20 @@ function PullRequestsColumn({...@@ -1860,20 +1866,20 @@ function PullRequestsColumn({
1860 >1866 >
1861 {titlebarControls}1867 {titlebarControls}
1862 {condensed ? (1868 {condensed ? (
1863 <WorkspaceBreadcrumb ariaLabel="Pull request scope">1869 <WorkspaceBreadcrumb ariaLabel="Pull request scope" className="overflow-hidden">
1864 {/* The page name remains the foreground anchor in both states; the live filters are1870 {/* An expanded search owns the scarce horizontal space. The page title stays
1865 its compact scope, grouped as the second crumb rather than pretending each menu1871 available to readers while the live filters remain available in both states. */}
1866 is a separate page in the hierarchy. */}1872 <WorkspaceBreadcrumbItem current className={cn(searchExpanded && "sr-only")}>
1867 <WorkspaceBreadcrumbItem current>
1868 <h1 className="truncate">Pull Requests</h1>1873 <h1 className="truncate">Pull Requests</h1>
1869 </WorkspaceBreadcrumbItem>1874 </WorkspaceBreadcrumbItem>
1870 <WorkspaceBreadcrumbSeparator />1875 {searchExpanded ? null : <WorkspaceBreadcrumbSeparator />}
1871 <WorkspaceBreadcrumbItem className="gap-1.5 overflow-hidden">1876 <WorkspaceBreadcrumbItem className="shrink gap-1.5">
1872 <CompactFilterMenu1877 <CompactFilterMenu
1873 label="Filter by state"1878 label="Filter by state"
1874 value={state}1879 value={state}
1875 options={STATE_TABS}1880 options={STATE_TABS}
1876 onChange={onState}1881 onChange={onState}
1882 className="shrink-0"
1877 />1883 />
1878 <CompactFilterMenu1884 <CompactFilterMenu
1879 label="Filter by involvement"1885 label="Filter by involvement"
...@@ -1900,7 +1906,7 @@ function PullRequestsColumn({...@@ -1900,7 +1906,7 @@ function PullRequestsColumn({
1900 )}1906 )}
1901 <div className="min-w-0 flex-1" />1907 <div className="min-w-0 flex-1" />
1902 {condensed ? (1908 {condensed ? (
1903 <div className="flex shrink-0 items-center gap-1.5">1909 <div className="flex shrink items-center gap-1.5">
1904 <ExpandableSearch1910 <ExpandableSearch
1905 searchInput={searchInput}1911 searchInput={searchInput}
1906 searchValue={searchValue}1912 searchValue={searchValue}