diff --git a/apps/web/src/components/pullRequest/PullRequestDetailLayout.tsx b/apps/web/src/components/pullRequest/PullRequestDetailLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..92bd0ab324d517560729be9e1144f87404e42b2b
--- /dev/null
+++ b/apps/web/src/components/pullRequest/PullRequestDetailLayout.tsx
@@ -0,0 +1,130 @@
+import { ArrowLeftIcon, FileDiffIcon } from "lucide-react";
+import type { ComponentProps, ReactNode } from "react";
+
+import { cn } from "~/lib/utils";
+
+import { Toggle, ToggleGroup } from "../ui/toggle-group";
+import { PullRequestMetaLine } from "./pullRequestPresentation";
+
+export function PullRequestDetailHeaderBody({
+ title,
+ author,
+ updated,
+ checkout,
+ base,
+ head,
+ files,
+ diffStat,
+}: {
+ title: ReactNode;
+ author: ReactNode;
+ updated: ReactNode;
+ checkout?: ReactNode;
+ base: ReactNode;
+ head: ReactNode;
+ files: ReactNode;
+ diffStat: ReactNode;
+}) {
+ return (
+
+ {title}
+
+
+ {author}
+ {updated}
+
+ {checkout}
+
+
+
+ {base}
+
+ {head}
+
+
+
+
+ {files}
+
+ {diffStat}
+
+
+
+ );
+}
+
+export function PullRequestDetailTitleRow({ className, ...props }: ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export function PullRequestDetailTabBar({
+ tabs,
+ value,
+ onValueChange,
+ onTabIntent,
+ inert,
+ children,
+}: {
+ tabs: ReadonlyArray<{ readonly value: Tab; readonly label: string }>;
+ value: Tab;
+ onValueChange?: (tab: Tab) => void;
+ onTabIntent?: (tab: Tab) => void;
+ inert?: boolean;
+ children?: ReactNode;
+}) {
+ return (
+
+ {
+ const nextTab = tabs.find((item) => item.value === next[0])?.value;
+ if (nextTab) onValueChange?.(nextTab);
+ }}
+ >
+ {tabs.map((tab) => (
+ onTabIntent(tab.value) : undefined}
+ onFocus={onTabIntent ? () => onTabIntent(tab.value) : undefined}
+ >
+ {tab.label}
+
+ ))}
+
+ {children}
+
+ );
+}
+
+export function PullRequestChecksStatusLine({
+ icon,
+ label,
+ className,
+ ...props
+}: Omit, "children"> & { icon: ReactNode; label: ReactNode }) {
+ return (
+
+ {icon}
+ {label}
+
+ );
+}
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
index f4489607398588ceb72d3af23f3258f645e1a2e2..7f6a4592a0b4f7f371f93827f7d25b6f0ec67898 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
@@ -102,7 +102,6 @@ import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { PullRequestEditButton } from "./PullRequestEditButton";
import { Input } from "../ui/input";
-import { Toggle, ToggleGroup } from "../ui/toggle-group";
import {
Menu,
MenuItem,
@@ -117,6 +116,12 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { MiddleTruncate } from "../ui/middle-truncate";
+import {
+ PullRequestChecksStatusLine,
+ PullRequestDetailHeaderBody,
+ PullRequestDetailTabBar,
+ PullRequestDetailTitleRow,
+} from "./PullRequestDetailLayout";
import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
@@ -2373,77 +2378,79 @@ export function PullRequestDetailPanel({
inert={condensed}
>
{detail ? (
-
- {titleDraft === null ? (
-
-
-
- {detail.title}
-
- }
- />
- {detail.title}
-
- {canEditPullRequestChangeRequest(detail) ? (
-
setTitleScope({ pullRequestKey, text: detail.title })}
- />
- ) : null}
-
- ) : (
- // A title is one line of text, not markdown, so it takes an input rather than
- // the editor the description and the remarks share.
-
-
- setTitleScope({ pullRequestKey, text: event.target.value })
- }
- onKeyDown={(event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- void saveTitle(titleDraft);
- } else if (event.key === "Escape") {
- event.preventDefault();
- setTitleScope(null);
- }
- }}
- />
-
-
+
+
+ {detail.title}
+
+ }
+ />
+ {detail.title}
+
+ {canEditPullRequestChangeRequest(detail) ? (
+ setTitleScope({ pullRequestKey, text: detail.title })}
+ />
+ ) : null}
+
+ ) : (
+ // A title is one line of text, not markdown, so it takes an input rather than
+ // the editor the description and the remarks share.
+
+
setTitleScope(null)}
- >
- Cancel
-
-
+ setTitleScope({ pullRequestKey, text: event.target.value })
}
- onClick={() => void saveTitle(titleDraft)}
- >
- {titleSaving ? "Saving..." : "Save"}
-
+ onKeyDown={(event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ void saveTitle(titleDraft);
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ setTitleScope(null);
+ }
+ }}
+ />
+
+ setTitleScope(null)}
+ >
+ Cancel
+
+ void saveTitle(titleDraft)}
+ >
+ {titleSaving ? "Saving..." : "Save"}
+
+
-
- )}
-
-
-
- updated {formatRelativeTimeLabel(detail.updatedAt)}
-
- {checkoutCommand ? (
+ )
+ }
+ author={
+
+ }
+ updated={
updated {formatRelativeTimeLabel(detail.updatedAt)} }
+ checkout={
+ checkoutCommand ? (
- ) : null}
-
-
-
-
- {/* An out-of-date base wears the warning on the branch name itself, so the
+ ) : null
+ }
+ base={
+ /* An out-of-date base wears the warning on the branch name itself, so the
name is amber and pointing at either the name or the mark opens the way
- out. Up to date, the name keeps its plain tooltip. */}
- {freshness ? (
- void perform("update-branch", undefined, method)}
- className="max-w-[40%]"
- >
- {isStackedPullRequest ? (
-
- ) : null}
-
-
-
-
- ) : (
-
-
- {isStackedPullRequest ? (
-
- ) : null}
-
-
-
-
- }
+ out. Up to date, the name keeps its plain tooltip. */
+ freshness ? (
+
void perform("update-branch", undefined, method)}
+ className="max-w-[40%]"
+ >
+ {isStackedPullRequest ? (
+
-
- {isStackedPullRequest
- ? `Stacked on ${detail.baseBranch}`
- : detail.baseBranch}
-
-
- )}
-
-
-
-
-
-
- {detail.changedFiles.toLocaleString()}{" "}
- {detail.changedFiles === 1 ? "file" : "files"}
-
-
-
-
-
+ ) : null}
+
+
+
+
+ ) : (
+
+
+ {isStackedPullRequest ? (
+
+ ) : null}
+
+
+
+
+ }
+ />
+
+ {isStackedPullRequest
+ ? `Stacked on ${detail.baseBranch}`
+ : detail.baseBranch}
+
+
+ )
+ }
+ head={
+
+ }
+ files={`${detail.changedFiles.toLocaleString()} ${detail.changedFiles === 1 ? "file" : "files"}`}
+ diffStat={
+
+ }
+ />
) : null}
{detail ? (
- {
+ if (item === "code") void loadCodeTab();
+ }}
>
- {
- const nextTab = visibleTabs.find((item) => item.value === next[0])?.value;
- if (nextTab) setTab(nextTab);
- }}
- >
- {visibleTabs.map((item) => (
- void loadCodeTab() : undefined}
- onFocus={item.value === "code" ? () => void loadCodeTab() : undefined}
- >
- {item.label}
-
- ))}
-
{tab === "summary" ? (
-
- {showsApproveWorkflows ? (
-
-
-
- setConfirmation({ open: true, action: "approve-workflows" })
- }
- aria-label={
- pendingAction === "approve-workflows"
- ? "Approving..."
- : "Approve workflows to run"
- }
- >
-
-
- {pendingAction === "approve-workflows"
- ? "Approving..."
- : "Approve workflows to run"}
-
-
-
- }
- />
-
- {pendingAction === "approve-workflows"
- ? "Approving..."
- : "Approve workflows to run"}
-
-
- ) : (
-
- {checksState !== null ? (
+ showsApproveWorkflows ? (
+
+
+
+ setConfirmation({ open: true, action: "approve-workflows" })
+ }
+ aria-label={
+ pendingAction === "approve-workflows"
+ ? "Approving..."
+ : "Approve workflows to run"
+ }
+ >
+
+
+ {pendingAction === "approve-workflows"
+ ? "Approving..."
+ : "Approve workflows to run"}
+
+
+
+ }
+ />
+
+ {pendingAction === "approve-workflows"
+ ? "Approving..."
+ : "Approve workflows to run"}
+
+
+ ) : (
+
) : (
- )}
- {checksSummary}
-
- )}
-
+ )
+ }
+ label={checksSummary}
+ />
+ )
) : tab === "timeline" ? (
) : null}
-
+
) : null}
diff --git a/apps/web/src/components/pullRequest/PullRequestGhosts.tsx b/apps/web/src/components/pullRequest/PullRequestGhosts.tsx
index 85c9802c21706ce34a6f6bd21b13a9c1bde91782..7e155882a34adf9f856b432e2081aadfe38b8f32 100644
--- a/apps/web/src/components/pullRequest/PullRequestGhosts.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestGhosts.tsx
@@ -13,7 +13,6 @@ import {
ChevronRightIcon,
EllipsisIcon,
ExternalLinkIcon,
- FileDiffIcon,
PanelRightIcon,
TagIcon,
UserPlusIcon,
@@ -26,20 +25,24 @@ import { cn } from "~/lib/utils";
import { formatRelativeTimeLabel } from "~/timestampFormat";
import { Button, InlineButton } from "../ui/button";
-import { Toggle, ToggleGroup } from "../ui/toggle-group";
import { MiddleTruncate } from "../ui/middle-truncate";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
+import {
+ PullRequestChecksStatusLine,
+ PullRequestDetailHeaderBody,
+ PullRequestDetailTabBar,
+ PullRequestDetailTitleRow,
+} from "./PullRequestDetailLayout";
import {
PullRequestActorLabel,
PullRequestDiffStat,
PullRequestLabelChip,
- PullRequestMetaLine,
pullRequestChecksStatePresentation,
resolvePullRequestState,
} from "./pullRequestPresentation";
function GhostBar({ className }: { className?: string | undefined }) {
- return
;
+ return ;
}
/** Widths cycle rather than randomize, so the ghost renders the same on every pass. */
@@ -202,32 +205,35 @@ export function PullRequestDetailGhost({
-
- {seed ? (
-
-
{seed.title}
-
- ) : (
-
-
-
- )}
-
- {seed ? (
-
-
- updated {formatRelativeTimeLabel(seed.updatedAt)}
-
+
+ {seed ? (
+ {seed.title}
+ ) : (
+
+ )}
+
+ }
+ author={
+ seed ? (
+
) : (
-
-
-
-
-
-
-
- )}
- {checkout ? (
+
+
+
+
+ )
+ }
+ updated={
+ seed ? (
+ updated {formatRelativeTimeLabel(seed.updatedAt)}
+ ) : (
+
+ )
+ }
+ checkout={
+ checkout ? (
- ) : null}
-
-
-
-
+ ) : null
+ }
+ base={
+
{seed ? (
-
-
-
-
-
+
+
+
) : (
-
-
-
- )}
-
- {seed ? (
-
- ) : (
-
- )}
-
-
-
-
- {changedFiles === null ? (
-
- ) : (
- `${changedFiles.toLocaleString()} ${changedFiles === 1 ? "file" : "files"}`
- )}
-
- {seed ? (
-
- ) : (
-
+
)}
-
-
+ }
+ head={
+ seed ? (
+
+ ) : (
+
+ )
+ }
+ files={
+ changedFiles === null ? (
+
+ ) : (
+ `${changedFiles.toLocaleString()} ${changedFiles === 1 ? "file" : "files"}`
+ )
+ }
+ diffStat={
+ seed ? (
+
+ ) : (
+
+ )
+ }
+ />
-
-
- {tabs.map((tab) => (
-
- {tab.label}
-
- ))}
-
+
{checksPresentation ? (
-
-
- {checksPresentation.label}
-
+ }
+ label={checksPresentation.label}
+ />
) : (
-
+ }
+ label={ }
+ />
)}
-
+