authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-07 21:18:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 01:18:29-03:00
log843c593ab2dfe432abfba7b928292f3f8e9c7d44
tree2597de99ee9b35dffdb1e4b1102499d39e5ce1d8
parent6f83358ae6d85b5dee7d3280e645e0e9a0aeff62
signature Signed by PGP key B5690EEEBB952194

refactor(web): PR loading skeleton shares the detail panel's layout (#15583)


3 files changed, 412 insertions(+), 326 deletions(-)

apps/web/src/components/pullRequest/PullRequestDetailLayout.tsx created+130
......@@ -0,0 +1,130 @@
1import { ArrowLeftIcon, FileDiffIcon } from "lucide-react";
2import type { ComponentProps, ReactNode } from "react";
3
4import { cn } from "~/lib/utils";
5
6import { Toggle, ToggleGroup } from "../ui/toggle-group";
7import { PullRequestMetaLine } from "./pullRequestPresentation";
8
9export function PullRequestDetailHeaderBody({
10 title,
11 author,
12 updated,
13 checkout,
14 base,
15 head,
16 files,
17 diffStat,
18}: {
19 title: ReactNode;
20 author: ReactNode;
21 updated: ReactNode;
22 checkout?: ReactNode;
23 base: ReactNode;
24 head: ReactNode;
25 files: ReactNode;
26 diffStat: ReactNode;
27}) {
28 return (
29 <div className="col-span-2 mt-1 min-w-0 px-4 pb-4">
30 {title}
31 <div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
32 <PullRequestMetaLine className="min-w-0 whitespace-nowrap">
33 {author}
34 {updated}
35 </PullRequestMetaLine>
36 {checkout}
37 </div>
38 <div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
39 <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
40 {base}
41 <ArrowLeftIcon
42 aria-label="receives changes from"
43 className="size-3.5 shrink-0 opacity-60"
44 />
45 {head}
46 </span>
47 <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2">
48 <span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums">
49 <FileDiffIcon aria-hidden className="size-3.5" />
50 {files}
51 </span>
52 {diffStat}
53 </span>
54 </div>
55 </div>
56 );
57}
58
59export function PullRequestDetailTitleRow({ className, ...props }: ComponentProps<"div">) {
60 return (
61 <div className={cn("flex min-h-7 min-w-0 items-center sm:min-h-6", className)} {...props} />
62 );
63}
64
65export function PullRequestDetailTabBar<Tab extends string>({
66 tabs,
67 value,
68 onValueChange,
69 onTabIntent,
70 inert,
71 children,
72}: {
73 tabs: ReadonlyArray<{ readonly value: Tab; readonly label: string }>;
74 value: Tab;
75 onValueChange?: (tab: Tab) => void;
76 onTabIntent?: (tab: Tab) => void;
77 inert?: boolean;
78 children?: ReactNode;
79}) {
80 return (
81 <nav
82 className="col-span-2 flex min-w-0 flex-wrap items-center gap-2 border-t border-border/60 px-4 py-2"
83 aria-label="Pull request tabs"
84 inert={inert}
85 >
86 <ToggleGroup
87 className="shrink-0"
88 size="segmented"
89 variant="segmented"
90 value={[value]}
91 onValueChange={(next) => {
92 const nextTab = tabs.find((item) => item.value === next[0])?.value;
93 if (nextTab) onValueChange?.(nextTab);
94 }}
95 >
96 {tabs.map((tab) => (
97 <Toggle
98 key={tab.value}
99 value={tab.value}
100 onPointerEnter={onTabIntent ? () => onTabIntent(tab.value) : undefined}
101 onFocus={onTabIntent ? () => onTabIntent(tab.value) : undefined}
102 >
103 {tab.label}
104 </Toggle>
105 ))}
106 </ToggleGroup>
107 {children}
108 </nav>
109 );
110}
111
112export function PullRequestChecksStatusLine({
113 icon,
114 label,
115 className,
116 ...props
117}: Omit<ComponentProps<"span">, "children"> & { icon: ReactNode; label: ReactNode }) {
118 return (
119 <span
120 className={cn(
121 "ml-auto flex h-4 min-w-0 flex-1 flex-wrap content-start items-center justify-end gap-x-1.5 overflow-hidden text-xs",
122 className,
123 )}
124 {...props}
125 >
126 {icon}
127 <span className="whitespace-nowrap">{label}</span>
128 </span>
129 );
130}
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+194-219
......@@ -102,7 +102,6 @@ import { Badge } from "../ui/badge";
102102import { Button } from "../ui/button";
103103import { PullRequestEditButton } from "./PullRequestEditButton";
104104import { Input } from "../ui/input";
105import { Toggle, ToggleGroup } from "../ui/toggle-group";
106105import {
107106 Menu,
108107 MenuItem,
......@@ -117,6 +116,12 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
117116import { toastManager } from "../ui/toast";
118117import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
119118import { MiddleTruncate } from "../ui/middle-truncate";
119import {
120 PullRequestChecksStatusLine,
121 PullRequestDetailHeaderBody,
122 PullRequestDetailTabBar,
123 PullRequestDetailTitleRow,
124} from "./PullRequestDetailLayout";
120125import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
121126import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
122127import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
......@@ -2373,77 +2378,79 @@ export function PullRequestDetailPanel({
23732378 inert={condensed}
23742379 >
23752380 {detail ? (
2376 <div className="col-span-2 mt-1 min-w-0 px-4 pb-4">
2377 {titleDraft === null ? (
2378 <div className="group flex min-h-7 min-w-0 items-center gap-1 sm:min-h-6">
2379 <Tooltip>
2380 <TooltipTrigger
2381 render={
2382 <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug">
2383 {detail.title}
2384 </h1>
2385 }
2386 />
2387 <TooltipPopup side="top">{detail.title}</TooltipPopup>
2388 </Tooltip>
2389 {canEditPullRequestChangeRequest(detail) ? (
2390 <PullRequestEditButton
2391 aria-label="Edit title"
2392 onClick={() => setTitleScope({ pullRequestKey, text: detail.title })}
2393 />
2394 ) : null}
2395 </div>
2396 ) : (
2397 // A title is one line of text, not markdown, so it takes an input rather than
2398 // the editor the description and the remarks share.
2399 <div className="space-y-2">
2400 <Input
2401 autoFocus
2402 size="sm"
2403 disabled={titleSaving}
2404 value={titleDraft}
2405 aria-label="Pull request title"
2406 onChange={(event) =>
2407 setTitleScope({ pullRequestKey, text: event.target.value })
2408 }
2409 onKeyDown={(event) => {
2410 if (event.key === "Enter") {
2411 event.preventDefault();
2412 void saveTitle(titleDraft);
2413 } else if (event.key === "Escape") {
2414 event.preventDefault();
2415 setTitleScope(null);
2416 }
2417 }}
2418 />
2419 <div className="flex justify-end gap-2">
2420 <Button
2421 size="xs"
2422 variant="ghost"
2381 <PullRequestDetailHeaderBody
2382 title={
2383 titleDraft === null ? (
2384 <PullRequestDetailTitleRow className="group gap-1">
2385 <Tooltip>
2386 <TooltipTrigger
2387 render={
2388 <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug">
2389 {detail.title}
2390 </h1>
2391 }
2392 />
2393 <TooltipPopup side="top">{detail.title}</TooltipPopup>
2394 </Tooltip>
2395 {canEditPullRequestChangeRequest(detail) ? (
2396 <PullRequestEditButton
2397 aria-label="Edit title"
2398 onClick={() => setTitleScope({ pullRequestKey, text: detail.title })}
2399 />
2400 ) : null}
2401 </PullRequestDetailTitleRow>
2402 ) : (
2403 // A title is one line of text, not markdown, so it takes an input rather than
2404 // the editor the description and the remarks share.
2405 <div className="space-y-2">
2406 <Input
2407 autoFocus
2408 size="sm"
24232409 disabled={titleSaving}
2424 onClick={() => setTitleScope(null)}
2425 >
2426 Cancel
2427 </Button>
2428 <Button
2429 size="xs"
2430 variant="outline"
2431 disabled={
2432 !canWriteSourceControl || titleSaving || titleDraft.trim().length === 0
2410 value={titleDraft}
2411 aria-label="Pull request title"
2412 onChange={(event) =>
2413 setTitleScope({ pullRequestKey, text: event.target.value })
24332414 }
2434 onClick={() => void saveTitle(titleDraft)}
2435 >
2436 {titleSaving ? "Saving..." : "Save"}
2437 </Button>
2415 onKeyDown={(event) => {
2416 if (event.key === "Enter") {
2417 event.preventDefault();
2418 void saveTitle(titleDraft);
2419 } else if (event.key === "Escape") {
2420 event.preventDefault();
2421 setTitleScope(null);
2422 }
2423 }}
2424 />
2425 <div className="flex justify-end gap-2">
2426 <Button
2427 size="xs"
2428 variant="ghost"
2429 disabled={titleSaving}
2430 onClick={() => setTitleScope(null)}
2431 >
2432 Cancel
2433 </Button>
2434 <Button
2435 size="xs"
2436 variant="outline"
2437 disabled={
2438 !canWriteSourceControl || titleSaving || titleDraft.trim().length === 0
2439 }
2440 onClick={() => void saveTitle(titleDraft)}
2441 >
2442 {titleSaving ? "Saving..." : "Save"}
2443 </Button>
2444 </div>
24382445 </div>
2439 </div>
2440 )}
2441 <div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
2442 <PullRequestMetaLine className="min-w-0 whitespace-nowrap">
2443 <PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} />
2444 <span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span>
2445 </PullRequestMetaLine>
2446 {checkoutCommand ? (
2446 )
2447 }
2448 author={
2449 <PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} />
2450 }
2451 updated={<span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span>}
2452 checkout={
2453 checkoutCommand ? (
24472454 <PullRequestCopyableCode
24482455 key={checkoutCommand}
24492456 value={checkoutCommand}
......@@ -2454,159 +2461,127 @@ export function PullRequestDetailPanel({
24542461 tooltipSide="bottom"
24552462 onError={onCheckoutCommandError}
24562463 />
2457 ) : null}
2458 </div>
2459
2460 <div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
2461 <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
2462 {/* An out-of-date base wears the warning on the branch name itself, so the
2464 ) : null
2465 }
2466 base={
2467 /* An out-of-date base wears the warning on the branch name itself, so the
24632468 name is amber and pointing at either the name or the mark opens the way
2464 out. Up to date, the name keeps its plain tooltip. */}
2465 {freshness ? (
2466 <PullRequestBaseFreshnessWarning
2467 baseBranch={detail.baseBranch}
2468 freshness={freshness}
2469 pending={actionPending}
2470 onUpdate={(method) => void perform("update-branch", undefined, method)}
2471 className="max-w-[40%]"
2472 >
2473 {isStackedPullRequest ? (
2474 <PullRequestGlyph.stack
2475 aria-label="Stacked pull request"
2476 className="size-3 shrink-0"
2477 />
2478 ) : null}
2479 <code className="flex min-w-0">
2480 <MiddleTruncate value={detail.baseBranch} showTitle={false} />
2481 </code>
2482 </PullRequestBaseFreshnessWarning>
2483 ) : (
2484 <Tooltip>
2485 <TooltipTrigger
2486 render={
2487 <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
2488 {isStackedPullRequest ? (
2489 <PullRequestGlyph.stack
2490 aria-label="Stacked pull request"
2491 className="size-3 shrink-0"
2492 />
2493 ) : null}
2494 <code className="flex min-w-0">
2495 <MiddleTruncate value={detail.baseBranch} showTitle={false} />
2496 </code>
2497 </span>
2498 }
2469 out. Up to date, the name keeps its plain tooltip. */
2470 freshness ? (
2471 <PullRequestBaseFreshnessWarning
2472 baseBranch={detail.baseBranch}
2473 freshness={freshness}
2474 pending={actionPending}
2475 onUpdate={(method) => void perform("update-branch", undefined, method)}
2476 className="max-w-[40%]"
2477 >
2478 {isStackedPullRequest ? (
2479 <PullRequestGlyph.stack
2480 aria-label="Stacked pull request"
2481 className="size-3 shrink-0"
24992482 />
2500 <TooltipPopup side="top">
2501 {isStackedPullRequest
2502 ? `Stacked on ${detail.baseBranch}`
2503 : detail.baseBranch}
2504 </TooltipPopup>
2505 </Tooltip>
2506 )}
2507 <ArrowLeftIcon
2508 aria-label="receives changes from"
2509 className="size-3.5 shrink-0 opacity-60"
2510 />
2511 <PullRequestCopyableCode
2512 key={detail.headBranch}
2513 value={detail.headBranch}
2514 target="branch name"
2515 copyLabel="Copy pull request branch"
2516 copiedLabel="Branch name copied"
2517 />
2518 </span>
2519 <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2">
2520 <span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums">
2521 <FileDiffIcon className="size-3.5" />
2522 {detail.changedFiles.toLocaleString()}{" "}
2523 {detail.changedFiles === 1 ? "file" : "files"}
2524 </span>
2525 <PullRequestDiffStat
2526 additions={detail.additions}
2527 deletions={detail.deletions}
2528 className="shrink-0 font-mono text-xs"
2529 />
2530 </span>
2531 </div>
2532 </div>
2483 ) : null}
2484 <code className="flex min-w-0">
2485 <MiddleTruncate value={detail.baseBranch} showTitle={false} />
2486 </code>
2487 </PullRequestBaseFreshnessWarning>
2488 ) : (
2489 <Tooltip>
2490 <TooltipTrigger
2491 render={
2492 <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
2493 {isStackedPullRequest ? (
2494 <PullRequestGlyph.stack
2495 aria-label="Stacked pull request"
2496 className="size-3 shrink-0"
2497 />
2498 ) : null}
2499 <code className="flex min-w-0">
2500 <MiddleTruncate value={detail.baseBranch} showTitle={false} />
2501 </code>
2502 </span>
2503 }
2504 />
2505 <TooltipPopup side="top">
2506 {isStackedPullRequest
2507 ? `Stacked on ${detail.baseBranch}`
2508 : detail.baseBranch}
2509 </TooltipPopup>
2510 </Tooltip>
2511 )
2512 }
2513 head={
2514 <PullRequestCopyableCode
2515 key={detail.headBranch}
2516 value={detail.headBranch}
2517 target="branch name"
2518 copyLabel="Copy pull request branch"
2519 copiedLabel="Branch name copied"
2520 />
2521 }
2522 files={`${detail.changedFiles.toLocaleString()} ${detail.changedFiles === 1 ? "file" : "files"}`}
2523 diffStat={
2524 <PullRequestDiffStat
2525 additions={detail.additions}
2526 deletions={detail.deletions}
2527 className="shrink-0 font-mono text-xs"
2528 />
2529 }
2530 />
25332531 ) : null}
25342532 </div>
25352533 </div>
25362534
25372535 {detail ? (
2538 <nav
2539 className="col-span-2 flex min-w-0 flex-wrap items-center gap-2 border-t border-border/60 px-4 py-2"
2540 aria-label="Pull request tabs"
2536 <PullRequestDetailTabBar
2537 tabs={visibleTabs}
2538 value={tab}
2539 onValueChange={setTab}
2540 onTabIntent={(item) => {
2541 if (item === "code") void loadCodeTab();
2542 }}
25412543 >
2542 <ToggleGroup
2543 className="shrink-0"
2544 size="segmented"
2545 variant="segmented"
2546 value={[tab]}
2547 onValueChange={(next) => {
2548 const nextTab = visibleTabs.find((item) => item.value === next[0])?.value;
2549 if (nextTab) setTab(nextTab);
2550 }}
2551 >
2552 {visibleTabs.map((item) => (
2553 <Toggle
2554 key={item.value}
2555 value={item.value}
2556 onPointerEnter={item.value === "code" ? () => void loadCodeTab() : undefined}
2557 onFocus={item.value === "code" ? () => void loadCodeTab() : undefined}
2558 >
2559 {item.label}
2560 </Toggle>
2561 ))}
2562 </ToggleGroup>
25632544 {tab === "summary" ? (
2564 <span
2565 className={cn(
2566 "ml-auto flex items-center justify-end",
2567 showsApproveWorkflows ? "shrink-0" : "min-w-0 flex-1",
2568 )}
2569 >
2570 {showsApproveWorkflows ? (
2571 <Tooltip>
2572 <TooltipTrigger
2573 render={
2574 <span className="inline-flex shrink-0">
2575 <Button
2576 size="xs"
2577 variant="warning-outline"
2578 disabled={actionPending}
2579 onClick={() =>
2580 setConfirmation({ open: true, action: "approve-workflows" })
2581 }
2582 aria-label={
2583 pendingAction === "approve-workflows"
2584 ? "Approving..."
2585 : "Approve workflows to run"
2586 }
2587 >
2588 <PlayIcon aria-hidden className="size-3.5" />
2589 <span>
2590 {pendingAction === "approve-workflows"
2591 ? "Approving..."
2592 : "Approve workflows to run"}
2593 </span>
2594 </Button>
2595 </span>
2596 }
2597 />
2598 <TooltipPopup side="top">
2599 {pendingAction === "approve-workflows"
2600 ? "Approving..."
2601 : "Approve workflows to run"}
2602 </TooltipPopup>
2603 </Tooltip>
2604 ) : (
2605 <span
2606 className="flex h-4 min-w-0 flex-wrap content-start items-center justify-end gap-x-1.5 overflow-hidden text-xs text-muted-foreground"
2607 aria-label={checksSummary ? `Checks: ${checksSummary}` : "Checks"}
2608 >
2609 {checksState !== null ? (
2545 showsApproveWorkflows ? (
2546 <Tooltip>
2547 <TooltipTrigger
2548 render={
2549 <span className="ml-auto inline-flex shrink-0">
2550 <Button
2551 size="xs"
2552 variant="warning-outline"
2553 disabled={actionPending}
2554 onClick={() =>
2555 setConfirmation({ open: true, action: "approve-workflows" })
2556 }
2557 aria-label={
2558 pendingAction === "approve-workflows"
2559 ? "Approving..."
2560 : "Approve workflows to run"
2561 }
2562 >
2563 <PlayIcon aria-hidden className="size-3.5" />
2564 <span>
2565 {pendingAction === "approve-workflows"
2566 ? "Approving..."
2567 : "Approve workflows to run"}
2568 </span>
2569 </Button>
2570 </span>
2571 }
2572 />
2573 <TooltipPopup side="top">
2574 {pendingAction === "approve-workflows"
2575 ? "Approving..."
2576 : "Approve workflows to run"}
2577 </TooltipPopup>
2578 </Tooltip>
2579 ) : (
2580 <PullRequestChecksStatusLine
2581 className="text-muted-foreground"
2582 aria-label={checksSummary ? `Checks: ${checksSummary}` : "Checks"}
2583 icon={
2584 checksState !== null ? (
26102585 <PullRequestChecksPopover
26112586 checks={detail.checks}
26122587 stale={checksStale}
......@@ -2615,11 +2590,11 @@ export function PullRequestDetailPanel({
26152590 />
26162591 ) : (
26172592 <CircleDotIcon aria-hidden className="size-3.5" />
2618 )}
2619 <span className="whitespace-nowrap">{checksSummary}</span>
2620 </span>
2621 )}
2622 </span>
2593 )
2594 }
2595 label={checksSummary}
2596 />
2597 )
26232598 ) : tab === "timeline" ? (
26242599 <div className="ml-auto flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
26252600 <PullRequestMetaLine
......@@ -2694,7 +2669,7 @@ export function PullRequestDetailPanel({
26942669 </Button>
26952670 </div>
26962671 ) : null}
2697 </nav>
2672 </PullRequestDetailTabBar>
26982673 ) : null}
26992674 </div>
27002675
apps/web/src/components/pullRequest/PullRequestGhosts.tsx+88-107
......@@ -13,7 +13,6 @@ import {
1313 ChevronRightIcon,
1414 EllipsisIcon,
1515 ExternalLinkIcon,
16 FileDiffIcon,
1716 PanelRightIcon,
1817 TagIcon,
1918 UserPlusIcon,
......@@ -26,20 +25,24 @@ import { cn } from "~/lib/utils";
2625import { formatRelativeTimeLabel } from "~/timestampFormat";
2726
2827import { Button, InlineButton } from "../ui/button";
29import { Toggle, ToggleGroup } from "../ui/toggle-group";
3028import { MiddleTruncate } from "../ui/middle-truncate";
3129import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
30import {
31 PullRequestChecksStatusLine,
32 PullRequestDetailHeaderBody,
33 PullRequestDetailTabBar,
34 PullRequestDetailTitleRow,
35} from "./PullRequestDetailLayout";
3236import {
3337 PullRequestActorLabel,
3438 PullRequestDiffStat,
3539 PullRequestLabelChip,
36 PullRequestMetaLine,
3740 pullRequestChecksStatePresentation,
3841 resolvePullRequestState,
3942} from "./pullRequestPresentation";
4043
4144function GhostBar({ className }: { className?: string | undefined }) {
42 return <div aria-hidden className={cn("h-3 rounded bg-muted-foreground/15", className)} />;
45 return <span aria-hidden className={cn("block h-3 rounded bg-muted-foreground/15", className)} />;
4346}
4447
4548/** Widths cycle rather than randomize, so the ghost renders the same on every pass. */
......@@ -202,32 +205,35 @@ export function PullRequestDetailGhost({
202205
203206 <div className="col-span-2 grid grid-rows-[1fr]">
204207 <div className="min-h-0 overflow-hidden">
205 <div className="col-span-2 mt-1 min-w-0 px-4 pb-4">
206 {seed ? (
207 <div className="flex min-h-7 min-w-0 items-center sm:min-h-6">
208 <h1 className="truncate text-base font-semibold leading-snug">{seed.title}</h1>
209 </div>
210 ) : (
211 <div className="flex min-h-7 min-w-0 items-center sm:min-h-6">
212 <GhostBar className="h-5 w-4/5 max-w-md" />
213 </div>
214 )}
215 <div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
216 {seed ? (
217 <PullRequestMetaLine className="min-w-0 whitespace-nowrap">
218 <PullRequestActorLabel actor={seed.author ?? null} tooltip={false} />
219 <span>updated {formatRelativeTimeLabel(seed.updatedAt)}</span>
220 </PullRequestMetaLine>
208 <PullRequestDetailHeaderBody
209 title={
210 <PullRequestDetailTitleRow>
211 {seed ? (
212 <h1 className="truncate text-base font-semibold leading-snug">{seed.title}</h1>
213 ) : (
214 <GhostBar className="h-5 w-4/5 max-w-md" />
215 )}
216 </PullRequestDetailTitleRow>
217 }
218 author={
219 seed ? (
220 <PullRequestActorLabel actor={seed.author ?? null} tooltip={false} />
221221 ) : (
222 <PullRequestMetaLine className="min-w-0 whitespace-nowrap">
223 <span className="flex items-center gap-1.5">
224 <GhostBar className="size-4 rounded-full" />
225 <GhostBar className="h-3 w-14" />
226 </span>
227 <GhostBar className="h-3 w-16" />
228 </PullRequestMetaLine>
229 )}
230 {checkout ? (
222 <span className="flex items-center gap-1.5">
223 <GhostBar className="size-4 rounded-full" />
224 <GhostBar className="h-3 w-14" />
225 </span>
226 )
227 }
228 updated={
229 seed ? (
230 <span>updated {formatRelativeTimeLabel(seed.updatedAt)}</span>
231 ) : (
232 <GhostBar className="h-3 w-16" />
233 )
234 }
235 checkout={
236 checkout ? (
231237 <PullRequestCopyableCode
232238 key={checkout}
233239 value={checkout}
......@@ -238,94 +244,69 @@ export function PullRequestDetailGhost({
238244 tooltipSide="bottom"
239245 {...(onCheckoutError ? { onError: onCheckoutError } : {})}
240246 />
241 ) : null}
242 </div>
243
244 <div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
245 <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
246 {seed ? (
247 <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
248 <code className="flex min-w-0">
249 <MiddleTruncate value={seed.baseBranch} />
250 </code>
251 </span>
252 ) : (
253 <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
254 <GhostBar className="h-3 w-12" />
255 </span>
256 )}
257 <ArrowLeftIcon
258 aria-label="receives changes from"
259 className="size-3.5 shrink-0 opacity-60"
260 />
261 {seed ? (
262 <PullRequestCopyableCode
263 key={seed.headBranch}
264 value={seed.headBranch}
265 target="branch name"
266 copyLabel="Copy pull request branch"
267 copiedLabel="Branch name copied"
268 className="min-w-0 font-mono"
269 />
270 ) : (
271 <GhostBar className="h-3 w-32 flex-1" />
272 )}
273 </span>
274 <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2">
275 <span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums">
276 <FileDiffIcon aria-hidden className="size-3.5" />
277 {changedFiles === null ? (
278 <GhostBar className="h-3 w-10" />
279 ) : (
280 `${changedFiles.toLocaleString()} ${changedFiles === 1 ? "file" : "files"}`
281 )}
282 </span>
247 ) : null
248 }
249 base={
250 <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
283251 {seed ? (
284 <PullRequestDiffStat
285 additions={seed.additions ?? 0}
286 deletions={seed.deletions ?? 0}
287 className="shrink-0 font-mono text-xs"
288 />
252 <code className="flex min-w-0">
253 <MiddleTruncate value={seed.baseBranch} />
254 </code>
289255 ) : (
290 <GhostBar className="h-3 w-20" />
256 <GhostBar className="h-3 w-12" />
291257 )}
292258 </span>
293 </div>
294 </div>
259 }
260 head={
261 seed ? (
262 <PullRequestCopyableCode
263 key={seed.headBranch}
264 value={seed.headBranch}
265 target="branch name"
266 copyLabel="Copy pull request branch"
267 copiedLabel="Branch name copied"
268 className="min-w-0 font-mono"
269 />
270 ) : (
271 <GhostBar className="h-3 w-32 flex-1" />
272 )
273 }
274 files={
275 changedFiles === null ? (
276 <GhostBar className="h-3 w-10" />
277 ) : (
278 `${changedFiles.toLocaleString()} ${changedFiles === 1 ? "file" : "files"}`
279 )
280 }
281 diffStat={
282 seed ? (
283 <PullRequestDiffStat
284 additions={seed.additions ?? 0}
285 deletions={seed.deletions ?? 0}
286 className="shrink-0 font-mono text-xs"
287 />
288 ) : (
289 <GhostBar className="h-3 w-20" />
290 )
291 }
292 />
295293 </div>
296294 </div>
297295
298 <nav
299 className="col-span-2 flex min-w-0 flex-wrap items-center gap-2 border-t border-border/60 px-4 py-2"
300 aria-label="Pull request tabs"
301 inert
302 >
303 <ToggleGroup
304 className="shrink-0"
305 size="segmented"
306 variant="segmented"
307 value={[selectedTab]}
308 >
309 {tabs.map((tab) => (
310 <Toggle key={tab.value} value={tab.value} tabIndex={-1}>
311 {tab.label}
312 </Toggle>
313 ))}
314 </ToggleGroup>
296 <PullRequestDetailTabBar tabs={tabs} value={selectedTab} inert>
315297 {checksPresentation ? (
316 <span
317 className={cn(
318 "ml-auto inline-flex items-center gap-1.5 text-xs",
319 checksPresentation.toneClassName,
320 )}
321 >
322 <checksPresentation.Icon aria-hidden className="size-3.5" />
323 {checksPresentation.label}
324 </span>
298 <PullRequestChecksStatusLine
299 className={checksPresentation.toneClassName}
300 icon={<checksPresentation.Icon aria-hidden className="size-3.5" />}
301 label={checksPresentation.label}
302 />
325303 ) : (
326 <GhostBar className="ml-auto h-3 w-32" />
304 <PullRequestChecksStatusLine
305 icon={<GhostBar className="my-px size-3.5 rounded-full" />}
306 label={<GhostBar className="w-28" />}
307 />
327308 )}
328 </nav>
309 </PullRequestDetailTabBar>
329310 </div>
330311
331312 <div className="min-h-0 flex-1 overflow-hidden">