/** * Loading states specific to the pull request surface — the first list, a search under way, * and a detail panel opening — use bars in the geometry of the content they stand for, pulsing * on one composited layer. Diff loading uses the shared diff-panel skeleton instead. * * The bars share the app-wide `Skeleton` tone (`muted-foreground` at low alpha, which reads on * both themes) and the single `animate-skeleton` pulse, applied once on the container so any * number of bars costs one opacity animation. */ import type { PullRequestListEntry } from "@t3tools/contracts"; import { ArrowLeftIcon } from "lucide-react"; import { cn } from "~/lib/utils"; import { formatRelativeTimeLabel } from "~/timestampFormat"; import { pullRequestLabelColor } from "./pullRequestList.logic"; import { PullRequestActorLabel, PullRequestDiffStat, pullRequestChecksStatePresentation, resolvePullRequestState, } from "./pullRequestPresentation"; function GhostBar({ className }: { className?: string | undefined }) { return
; } /** Widths cycle rather than randomize, so the ghost renders the same on every pass. */ const TITLE_WIDTHS = ["w-3/5", "w-2/5", "w-1/2", "w-2/3", "w-2/5", "w-3/5", "w-1/2"]; const META_WIDTHS = ["w-2/5", "w-1/3", "w-2/5", "w-1/4", "w-1/3", "w-2/5", "w-1/3"]; /** Rows in the list's own grid — glyph, title over meta, time over diffstat. */ export function PullRequestListGhost({ rows = 7, caption, }: { rows?: number; /** Said where the group headers speak, for the states with something to say — a search. */ caption?: string; }) { return (
{caption ? (

{caption}

) : null} {Array.from({ length: rows }, (_, index) => (
))}
); } /** * The detail panel's current expanded shape. Keeping the chrome, summary facts, and description * boundaries in the ghost prevents the loaded pull request from replacing one layout with * another a moment later. */ export function PullRequestDetailGhost({ seed }: { seed?: PullRequestListEntry | null }) { const statePresentation = seed ? resolvePullRequestState({ state: seed.state, isDraft: seed.isDraft, }) : null; const checksPresentation = seed?.checksState ? pullRequestChecksStatePresentation(seed.checksState) : null; return (
{seed && statePresentation ? ( <> {seed.repository} #{seed.number} ) : ( <> )}
{seed ? (

{seed.title}

) : ( )}
{seed ? ( <> updated {formatRelativeTimeLabel(seed.updatedAt)} ) : ( <> )}
{seed ? ( {seed.baseBranch} {seed.headBranch} ) : ( <> )}
{seed ? ( ) : ( )}
{checksPresentation ? ( {checksPresentation.label} ) : ( )}
{seed ? ( seed.labels.slice(0, 3).map((label) => { const color = pullRequestLabelColor(label.color); return ( {label.name} ); }) ) : ( <> )}
); } /** People-shaped: an avatar and a name, in the reviewer picker's own row height. */ export function PullRequestPeopleGhost({ rows = 4 }: { rows?: number }) { return (
{Array.from({ length: rows }, (_, index) => (
))}
); } /** The timeline's own shape: dots on the rail, a line and a date to each. */ export function PullRequestTimelineGhost({ rows = 6 }: { rows?: number }) { return (
{Array.from({ length: rows }, (_, index) => (
))}
); } /** A compact placeholder for the conversation while the core detail is already readable. */ export function PullRequestConversationGhost({ rows = 3 }: { rows?: number }) { return (
{Array.from({ length: rows }, (_, index) => (
))}
); }