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 ( + + ); +} + +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); - } - }} - /> -
- - + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void saveTitle(titleDraft); + } else if (event.key === "Escape") { + event.preventDefault(); + setTitleScope(null); + } + }} + /> +
+ + +
-
- )} -
- - - 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 ? ( - + ) : 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 ? ( + + ) : ( + + ) + } + />
- +