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}