Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ such detail into a token. New layout spacing must use the scale.
identity ties. Recent first is one flat activity-ordered list labeling each
row with its repository. Rows retain compact tree density,
truncation, ages and metadata.
Show only current heads with changes. The current stream uses `--accent-bg`,
Show only current heads with changes; closed and merged GitHub pull requests
are not current. A pull request row shows a pull request (or draft) icon
labeled for assistive technology, `#N title`, and “GitHub · @author” first in
its metadata. The current stream uses `--accent-bg`,
accent text and `aria-current="page"`. Unopened arrivals show a small accent
dot labeled “New changes”; collapsed repositories carry their streams' dots.
Repository collapse state persists per browser.
Expand All @@ -323,6 +326,13 @@ such detail into a token. New layout spacing must use the scale.
`aria-current="true"` with accent text on `--selected` for the open review's
repository. A mark (“Open latest in {repo}”) opens that repository's most
active stream; the rail stays collapsed.
- A GitHub pull request review shows a banner on `--panel` above the review
notices: a state icon, muted `#N` and the semibold title, a status `Badge`
(Open success, Draft neutral, Merged accent, Closed error), `@author`, muted
monospace head → base, an outline-muted “Open on GitHub” `LinkButton`, and a
“Show description” disclosure, collapsed by default and persisted per
browser. The description is `MarkdownText`, bounded by `--reading-width`, and
scrolls within the banner rather than pushing the diff away.
- A newer head for the viewed stream shows a one-line `--accent-bg` notice below
the header with an outline-muted “View latest” action. Never swap the open review.
- Unopened arrivals are marked only by the column's and rail's accent dots and
Expand Down Expand Up @@ -433,6 +443,8 @@ with a semantic variant instead of overriding it.

| Need | Component (`apps/web/src/components`) |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Navigation styled as a button | `link-button` (keeps link semantics; `external` opens a new tab) |
| Untrusted Markdown prose | `markdown` (`MarkdownText`: no raw HTML, images as links) |
| Action, link-styled trigger | `ui/button` (variants: default, outline, outline-muted, outline-check, ghost, shell, rail, subtle, destructive, destructive-solid, link) |
| Icon-only action | `icon-button` (label is name and tooltip) |
| Primary action plus menu | `split-button` |
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"lucide-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-markdown": "catalog:",
"remark-gfm": "catalog:",
"shadcn": "catalog:",
"tw-animate-css": "catalog:"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FileExplorerNav } from "./FileExplorerNav.tsx";
import { Header } from "./Header.tsx";
import { Navigation } from "./Navigation.tsx";
import { NewerSnapshotNotice } from "./ProjectsNav.tsx";
import { PullRequestBanner } from "./PullRequestBanner.tsx";
import { CopyDialog } from "./review.tsx";
import { capabilityEnabled, useSession } from "./session-context.tsx";
import { useSidebarState } from "./sidebar-context.tsx";
Expand All @@ -22,6 +23,7 @@ export function App() {
<FileExplorerNav />
</Navigation>
<div className="workspace">
<PullRequestBanner />
<NewerSnapshotNotice />
<OutdatedCommentsNotice />
<DiffWorkspace />
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/ContextIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
type ProjectGroup,
type ProjectStream,
} from "./project-nav.ts";
import { NewMark, useNow } from "./ProjectsNav.tsx";
import { NewMark, StreamIcon, useNow } from "./ProjectsNav.tsx";
import { useCatalog } from "./session-context.tsx";
import { useShellPolicy } from "./shell-policy.ts";

Expand Down Expand Up @@ -204,7 +204,7 @@ function BranchItem({
return (
<ComboboxItem value={item} className="flex-col items-stretch gap-0">
<span className="flex min-w-0 items-center gap-1.5">
<GitBranchIcon aria-hidden="true" />
<StreamIcon context={context} />
<span className="min-w-0 flex-1 truncate">{streamTitle(context)}</span>
{unseen && <NewMark />}
{typeof context.changedFileCount === "number" && (
Expand Down
29 changes: 25 additions & 4 deletions apps/web/src/ProjectsNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ApiContext } from "@diffx/api";
import {
FolderGit2Icon,
GitBranchIcon,
GitPullRequestDraftIcon,
GitPullRequestIcon,
HistoryIcon,
ListIcon,
PanelLeftCloseIcon,
Expand Down Expand Up @@ -40,6 +42,7 @@ import {
latestStream,
newerHead,
projectGroups,
pullRequestOf,
recentStreams,
relativeAge,
repoMonogram,
Expand Down Expand Up @@ -471,10 +474,7 @@ function StreamRow({
onClick={(event) => onOpen(event, context)}
>
<span className="flex min-w-0 items-center gap-1.5">
<GitBranchIcon
className={current ? undefined : "text-muted"}
aria-hidden="true"
/>
<StreamIcon context={context} muted={!current} />
<span className="min-w-0 flex-1 truncate">{streamTitle(context)}</span>
{fresh && !current && <NewMark />}
{typeof context.changedFileCount === "number" && (
Expand All @@ -497,6 +497,27 @@ function StreamRow({
);
}

/** Pull requests carry their GitHub state; other streams show a branch. */
export function StreamIcon({
context,
muted = false,
}: {
context: ApiContext;
muted?: boolean;
}) {
const pull = pullRequestOf(context);
const className = muted ? "text-muted" : undefined;
if (!pull) return <GitBranchIcon className={className} aria-hidden="true" />;
const Icon = pull.draft ? GitPullRequestDraftIcon : GitPullRequestIcon;
return (
<Icon
className={className}
role="img"
aria-label={pull.draft ? "Draft pull request" : "Pull request"}
/>
);
}

export function NewMark() {
return <StatusDot tone="accent" size="sm" label="New changes" />;
}
Expand Down
107 changes: 107 additions & 0 deletions apps/web/src/PullRequestBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { ApiPullRequest } from "@diffx/api";
import {
ExternalLinkIcon,
GitMergeIcon,
GitPullRequestClosedIcon,
GitPullRequestDraftIcon,
GitPullRequestIcon,
type LucideIcon,
} from "lucide-react";
import { useId } from "react";
import { useSavedOpen } from "@/components/collapsible-section";
import { DisclosureIcon } from "@/components/disclosure";
import { IconButton } from "@/components/icon-button";
import { LinkButton } from "@/components/link-button";
import { MarkdownText } from "@/components/markdown";
import { Badge } from "@/components/ui/badge";
import { pullRequestOf } from "./project-nav.ts";
import { useSession } from "./session-context.tsx";

type Status = {
label: string;
tone: "accent" | "error" | "neutral" | "success";
Icon: LucideIcon;
};

/** State as GitHub names it; draft applies only while open. */
export function pullRequestStatus(pull: ApiPullRequest): Status {
if (pull.state === "merged")
return { label: "Merged", tone: "accent", Icon: GitMergeIcon };
if (pull.state === "closed")
return { label: "Closed", tone: "error", Icon: GitPullRequestClosedIcon };
if (pull.draft)
return { label: "Draft", tone: "neutral", Icon: GitPullRequestDraftIcon };
return { label: "Open", tone: "success", Icon: GitPullRequestIcon };
}

/**
* The open review's GitHub pull request above the diff: number, title, state,
* author and branches, with the description collapsed by default.
*/
export function PullRequestBanner() {
const session = useSession();
const pull = pullRequestOf(session);
const [open, setOpen] = useSavedOpen("pull-request-description", false);
const descriptionId = useId();
if (!pull) return null;
const status = pullRequestStatus(pull);
const body = pull.body.trim();
return (
<section
className="shrink-0 border-b border-border bg-panel text-sm"
aria-label={`Pull request #${pull.number}`}
>
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-(--diff-gutter) py-1">
<status.Icon className="size-icon-sm text-muted" aria-hidden="true" />
<h2 className="m-0 min-w-0 flex-1 truncate text-sm font-semibold">
<span className="font-mono font-normal text-muted">
#{pull.number}
</span>{" "}
{pull.title}
</h2>
<Badge variant="status" tone={status.tone}>
{status.label}
</Badge>
<span className="text-secondary">
<span className="sr-only">Author </span>@{pull.author.login}
</span>
<span
className="min-w-0 truncate font-mono text-xs text-muted"
title={`${pull.headRepository || pull.fullName}:${pull.headRef} into ${pull.baseRef}`}
>
<span className="sr-only">Merging </span>
{pull.headRef}
<span aria-hidden="true"> → </span>
<span className="sr-only"> into </span>
{pull.baseRef}
</span>
<LinkButton variant="outline-muted" size="sm" href={pull.url} external>
Open on GitHub
<ExternalLinkIcon className="size-icon-sm" aria-hidden="true" />
</LinkButton>
{body && (
<IconButton
size="icon-sm"
label={open ? "Hide description" : "Show description"}
aria-expanded={open}
aria-controls={descriptionId}
onClick={() => setOpen((value) => !value)}
>
<DisclosureIcon open={open} />
</IconButton>
)}
</div>
{body && (
<div
id={descriptionId}
hidden={!open}
className="pull-request-description px-(--diff-gutter) pt-1 pb-3"
>
<MarkdownText className="max-w-(--reading-width)">
{body}
</MarkdownText>
</div>
)}
</section>
);
}
Loading
Loading