| 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,3 +1,4 @@ |
| 1 | import { SearchIcon } from "lucide-react"; | ||
| 1 | import { memo } from "react"; | 2 | import { memo } from "react"; |
| 2 | 3 | ||
| 3 | import { cn } from "~/lib/utils"; | 4 | import { 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 the | 50 | // 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 the | 51 | // 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 of | 121 | {/* 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 | <span | 124 | <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({ |
| 339 | export function PullRequestActorLabel({ | 339 | export 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 | <MenuTrigger | 1643 | <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 | <div | 1727 | <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 the | 1817 | // 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 are | 1870 | {/* 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 menu | 1871 | 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 | <CompactFilterMenu | 1877 | <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 | <CompactFilterMenu | 1884 | <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 | <ExpandableSearch | 1910 | <ExpandableSearch |
| 1905 | searchInput={searchInput} | 1911 | searchInput={searchInput} |
| 1906 | searchValue={searchValue} | 1912 | searchValue={searchValue} |