| author | |
| committer | |
| log | cefec32d6fc5d14f03e110ebdde534bdbcc9b62b |
| tree | edf9a11d95b522a942595f34e33a5e38376f1c9c |
| parent | 352710d497cc640553e3e18e23fb5a5f3f890466 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 59 insertions(+), 29 deletions(-)
apps/web/src/components/pullRequest/PullRequestRow.tsx+38-16| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { SearchIcon } from "lucide-react"; | |
| 1 | 2 | import { memo } from "react"; |
| 2 | 3 | |
| 3 | 4 | import { cn } from "~/lib/utils"; |
| ... | ... | @@ -45,7 +46,7 @@ function PullRequestRowImpl({ |
| 45 | 46 | aria-current={selected ? "true" : undefined} |
| 46 | 47 | onClick={() => onSelect(entry)} |
| 47 | 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 | 50 | // Offscreen rows are skipped for style, layout and paint: a long list costs what the |
| 50 | 51 | // viewport shows, not what the pages have loaded. The intrinsic size keeps the |
| 51 | 52 | // scrollbar honest while a row is skipped. |
| ... | ... | @@ -59,9 +60,30 @@ function PullRequestRowImpl({ |
| 59 | 60 | mergeability={entry.mergeability} |
| 60 | 61 | baseBranch={entry.baseBranch} |
| 61 | 62 | /> |
| 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} | |
| 65 | 87 | <span className="flex shrink-0 items-center gap-1"> |
| 66 | 88 | {showProvider ? ( |
| 67 | 89 | <Tooltip> |
| ... | ... | @@ -89,15 +111,19 @@ function PullRequestRowImpl({ |
| 89 | 111 | </span> |
| 90 | 112 | {showProjectTitle ? <span className="truncate">{entry.repository}</span> : null} |
| 91 | 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 | 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 | 121 | {/* Only a verdict somebody has actually given: "review required" is the absence of |
| 96 | 122 | one, and saying so on every unreviewed row would say nothing. */} |
| 97 | 123 | {entry.reviewDecision === "approved" || entry.reviewDecision === "changes-requested" ? ( |
| 98 | 124 | <span |
| 99 | 125 | className={cn( |
| 100 | "shrink-0", | |
| 126 | "min-w-0 truncate", | |
| 101 | 127 | entry.reviewDecision === "approved" |
| 102 | 128 | ? "text-emerald-600/90 dark:text-emerald-400/80" |
| 103 | 129 | : "text-amber-600/90 dark:text-amber-400/80", |
| ... | ... | @@ -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 | 146 | </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 | /> | |
| 130 | 152 | </span> |
| 131 | 153 | </button> |
| 132 | 154 | ); |
apps/web/src/components/pullRequest/pullRequestPresentation.tsx+3-1| ... | ... | @@ -339,17 +339,19 @@ export function PullRequestActorAvatar({ |
| 339 | 339 | export function PullRequestActorLabel({ |
| 340 | 340 | actor, |
| 341 | 341 | className, |
| 342 | labelClassName, | |
| 342 | 343 | tooltip = true, |
| 343 | 344 | }: { |
| 344 | 345 | actor: PullRequestActor | null; |
| 345 | 346 | className?: string; |
| 347 | labelClassName?: string; | |
| 346 | 348 | tooltip?: boolean; |
| 347 | 349 | }) { |
| 348 | 350 | const login = actor?.login ?? "ghost"; |
| 349 | 351 | const label = ( |
| 350 | 352 | <> |
| 351 | 353 | <PullRequestActorAvatar actor={actor} /> |
| 352 | <span className="truncate">{login}</span> | |
| 354 | <span className={cn("truncate", labelClassName)}>{login}</span> | |
| 353 | 355 | </> |
| 354 | 356 | ); |
| 355 | 357 | if (!tooltip) { |
apps/web/src/routes/_chat.pull-requests.tsx+18-12| ... | ... | @@ -1628,11 +1628,13 @@ function CompactFilterMenu<Value extends string>({ |
| 1628 | 1628 | value, |
| 1629 | 1629 | options, |
| 1630 | 1630 | onChange, |
| 1631 | className, | |
| 1631 | 1632 | }: { |
| 1632 | 1633 | label: string; |
| 1633 | 1634 | value: Value; |
| 1634 | 1635 | options: ReadonlyArray<PullRequestFilterOption<Value>>; |
| 1635 | 1636 | onChange: (value: Value) => void; |
| 1637 | className?: string; | |
| 1636 | 1638 | }) { |
| 1637 | 1639 | const current = options.find((option) => option.value === value) ?? options[0]; |
| 1638 | 1640 | if (!current) return null; |
| ... | ... | @@ -1640,10 +1642,13 @@ function CompactFilterMenu<Value extends string>({ |
| 1640 | 1642 | <Menu> |
| 1641 | 1643 | <MenuTrigger |
| 1642 | 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} | |
| 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" /> | |
| 1647 | 1652 | </MenuTrigger> |
| 1648 | 1653 | <MenuPopup align="start" side="bottom" className="min-w-40"> |
| 1649 | 1654 | <MenuRadioGroup value={value} onValueChange={(next) => onChange(next as Value)}> |
| ... | ... | @@ -1721,7 +1726,7 @@ function ExpandableSearch({ |
| 1721 | 1726 | return ( |
| 1722 | 1727 | <div |
| 1723 | 1728 | ref={containerRef} |
| 1724 | className="w-56 shrink-0" | |
| 1729 | className="w-56 min-w-24 shrink" | |
| 1725 | 1730 | onFocus={() => onFocusWithin?.(true)} |
| 1726 | 1731 | onBlur={() => { |
| 1727 | 1732 | onFocusWithin?.(false); |
| ... | ... | @@ -1808,6 +1813,7 @@ function PullRequestsColumn({ |
| 1808 | 1813 | const inFlowSearchRef = useRef<HTMLDivElement | null>(null); |
| 1809 | 1814 | const [searchOpen, setSearchOpen] = useState(false); |
| 1810 | 1815 | const [searchFocusToken, setSearchFocusToken] = useState(0); |
| 1816 | const searchExpanded = searchOpen || searchValue.length > 0; | |
| 1811 | 1817 | // Mod+F belongs to this page's own search: the desktop shell binds no find-in-page, so the |
| 1812 | 1818 | // shortcut would otherwise do nothing. Condensed, it unfolds the topbar search; at the top, |
| 1813 | 1819 | // it focuses the in-flow bar and selects the query the way a find field would. |
| ... | ... | @@ -1860,20 +1866,20 @@ function PullRequestsColumn({ |
| 1860 | 1866 | > |
| 1861 | 1867 | {titlebarControls} |
| 1862 | 1868 | {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")}> | |
| 1868 | 1873 | <h1 className="truncate">Pull Requests</h1> |
| 1869 | 1874 | </WorkspaceBreadcrumbItem> |
| 1870 | <WorkspaceBreadcrumbSeparator /> | |
| 1871 | <WorkspaceBreadcrumbItem className="gap-1.5 overflow-hidden"> | |
| 1875 | {searchExpanded ? null : <WorkspaceBreadcrumbSeparator />} | |
| 1876 | <WorkspaceBreadcrumbItem className="shrink gap-1.5"> | |
| 1872 | 1877 | <CompactFilterMenu |
| 1873 | 1878 | label="Filter by state" |
| 1874 | 1879 | value={state} |
| 1875 | 1880 | options={STATE_TABS} |
| 1876 | 1881 | onChange={onState} |
| 1882 | className="shrink-0" | |
| 1877 | 1883 | /> |
| 1878 | 1884 | <CompactFilterMenu |
| 1879 | 1885 | label="Filter by involvement" |
| ... | ... | @@ -1900,7 +1906,7 @@ function PullRequestsColumn({ |
| 1900 | 1906 | )} |
| 1901 | 1907 | <div className="min-w-0 flex-1" /> |
| 1902 | 1908 | {condensed ? ( |
| 1903 | <div className="flex shrink-0 items-center gap-1.5"> | |
| 1909 | <div className="flex shrink items-center gap-1.5"> | |
| 1904 | 1910 | <ExpandableSearch |
| 1905 | 1911 | searchInput={searchInput} |
| 1906 | 1912 | searchValue={searchValue} |