mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
polish(desktop): detail-page layout cleanup for readme and commit views
README: wider reading inset, soft-wrap reflow, scrollable code blocks (new Markdown blockCode option), header row removed. Commit detail: full-width files pane that fills the remaining viewport via a flex chain instead of a fixed min-height, author shown in header and meta, tighter title spacing, link icon inline. Assign-to-me renders as a left-aligned link in the assignees row. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
@@ -267,7 +267,9 @@ export function IssueAssigneesRow({
|
||||
{canSelfAssign && viewer ? (
|
||||
<Button
|
||||
className={cn(
|
||||
"h-5 px-1 text-xs text-muted-foreground hover:text-foreground",
|
||||
contextActions
|
||||
? "h-5 px-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
: "h-5 px-0 text-xs text-primary hover:bg-transparent hover:text-primary hover:underline",
|
||||
contextActions && PROJECT_CONTEXT_ACTION_BUTTON_CLASS,
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-self-assign`}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Calendar, GitCommitHorizontal, Hash } from "lucide-react";
|
||||
import { Calendar, GitCommitHorizontal, Hash, UserRound } from "lucide-react";
|
||||
|
||||
import type { Repository } from "@/features/projects/hooks";
|
||||
import { commitDiscussionQuery } from "@/features/projects/lib/discussionChannels";
|
||||
@@ -48,86 +48,113 @@ export function ProjectCommitDetailPanel({
|
||||
project: Repository;
|
||||
}) {
|
||||
const shortHash = commit?.shortHash ?? commitHash.slice(0, 7);
|
||||
const fileCount = diff?.files.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={PROJECT_DETAIL_READING_COLUMN_CLASS}
|
||||
className="flex w-full flex-1 flex-col overflow-hidden"
|
||||
data-project-detail-panel
|
||||
data-testid="project-commit-detail"
|
||||
>
|
||||
<header className="space-y-2 px-6 pb-3 pt-5">
|
||||
<h3 className="line-clamp-2 text-lg font-semibold leading-6 text-foreground">
|
||||
{commit?.subject ?? shortHash}{" "}
|
||||
<ShareLinkButton
|
||||
className="ml-1 inline-flex h-7 w-7 align-text-bottom"
|
||||
label="Copy commit link"
|
||||
link={commitShareLink(project, commit?.hash ?? commitHash)}
|
||||
testId="project-commit-copy-link"
|
||||
/>
|
||||
</h3>
|
||||
<p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>Committed</span>
|
||||
{commit ? (
|
||||
<span
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
title={commitDateLabel(commit.timestamp)}
|
||||
>
|
||||
{commitDateLabel(commit.timestamp)}
|
||||
</span>
|
||||
) : null}
|
||||
<ProjectOriginReference
|
||||
agentName={originAgentName}
|
||||
channelId={originChannelId}
|
||||
/>
|
||||
</p>
|
||||
</header>
|
||||
<ProjectDetailMetaList>
|
||||
<ProjectDetailMetaRow icon={Hash} label="Commit">
|
||||
<span className="inline-flex min-w-0 items-center gap-1 font-mono text-xs">
|
||||
{shortHash}
|
||||
<CopyCommitHashButton
|
||||
className="h-6 w-6"
|
||||
hash={commit?.hash ?? commitHash}
|
||||
<div className={PROJECT_DETAIL_READING_COLUMN_CLASS}>
|
||||
<header className="space-y-1 px-6 pb-3 pt-5">
|
||||
<h3 className="line-clamp-2 text-lg font-semibold leading-6 text-foreground">
|
||||
{commit?.subject ?? shortHash}{" "}
|
||||
<ShareLinkButton
|
||||
className="ml-0.5 inline-flex h-auto w-auto align-middle hover:bg-transparent"
|
||||
label="Copy commit link"
|
||||
link={commitShareLink(project, commit?.hash ?? commitHash)}
|
||||
testId="project-commit-copy-link"
|
||||
/>
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
{commit ? (
|
||||
<ProjectDetailMetaRow icon={Calendar} label="Date">
|
||||
<span className="text-muted-foreground">
|
||||
{commitDateLabel(commit.timestamp)}
|
||||
</h3>
|
||||
<p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>Committed</span>
|
||||
{commit ? (
|
||||
<span
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
title={commitDateLabel(commit.timestamp)}
|
||||
>
|
||||
{commitDateLabel(commit.timestamp)}
|
||||
</span>
|
||||
) : null}
|
||||
{commit?.authorName ? (
|
||||
<span className="min-w-0 truncate" title={commit.authorEmail}>
|
||||
by <span className="text-foreground">{commit.authorName}</span>
|
||||
</span>
|
||||
) : null}
|
||||
<ProjectOriginReference
|
||||
agentName={originAgentName}
|
||||
channelId={originChannelId}
|
||||
/>
|
||||
</p>
|
||||
</header>
|
||||
<ProjectDetailMetaList>
|
||||
<ProjectDetailMetaRow icon={Hash} label="Commit">
|
||||
<span className="inline-flex min-w-0 items-center gap-1 font-mono text-xs">
|
||||
{shortHash}
|
||||
<CopyCommitHashButton
|
||||
className="h-6 w-6"
|
||||
hash={commit?.hash ?? commitHash}
|
||||
/>
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
{commit?.authorName ? (
|
||||
<ProjectDetailMetaRow icon={UserRound} label="Author">
|
||||
<span
|
||||
className="min-w-0 truncate text-muted-foreground"
|
||||
title={commit.authorEmail}
|
||||
>
|
||||
{commit.authorName}
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
) : null}
|
||||
{commit ? (
|
||||
<ProjectDetailMetaRow icon={Calendar} label="Date">
|
||||
<span className="text-muted-foreground">
|
||||
{commitDateLabel(commit.timestamp)}
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
) : null}
|
||||
{diff ? (
|
||||
<ProjectDetailMetaRow icon={GitCommitHorizontal} label="Changes">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="text-green-500">+{diff.additions}</span>
|
||||
<span className="text-destructive">-{diff.deletions}</span>
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
) : null}
|
||||
</ProjectDetailMetaList>
|
||||
{diff?.commitBody ? (
|
||||
<ProjectDetailSection defaultOpen title="Description">
|
||||
<ProjectRichContent
|
||||
content={diff.commitBody}
|
||||
hardLineBreaks={false}
|
||||
/>
|
||||
</ProjectDetailSection>
|
||||
) : null}
|
||||
{diff ? (
|
||||
<ProjectDetailMetaRow icon={GitCommitHorizontal} label="Changes">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="text-green-500">+{diff.additions}</span>
|
||||
<span className="text-destructive">-{diff.deletions}</span>
|
||||
</span>
|
||||
</ProjectDetailMetaRow>
|
||||
) : null}
|
||||
</ProjectDetailMetaList>
|
||||
{diff?.commitBody ? (
|
||||
<ProjectDetailSection defaultOpen title="Description">
|
||||
<ProjectRichContent
|
||||
content={diff.commitBody}
|
||||
hardLineBreaks={false}
|
||||
/>
|
||||
</ProjectDetailSection>
|
||||
) : null}
|
||||
<DiscussedInChannels
|
||||
className="mx-6 mb-4"
|
||||
entityLabel="this commit"
|
||||
query={commitDiscussionQuery({
|
||||
hash: commit?.hash ?? commitHash,
|
||||
shortHash: commit?.shortHash,
|
||||
})}
|
||||
testId="commit-discussed-in"
|
||||
/>
|
||||
<ProjectDetailSection count={fileCount} defaultOpen title="Files changed">
|
||||
<div className="-mx-6">
|
||||
<DiscussedInChannels
|
||||
className="mx-6 mb-4"
|
||||
entityLabel="this commit"
|
||||
query={commitDiscussionQuery({
|
||||
hash: commit?.hash ?? commitHash,
|
||||
shortHash: commit?.shortHash,
|
||||
})}
|
||||
testId="commit-discussed-in"
|
||||
/>
|
||||
</div>
|
||||
<ProjectDetailSection
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
contentClassName="flex min-h-0 flex-1 flex-col"
|
||||
defaultOpen
|
||||
headerClassName="mx-auto max-w-3xl"
|
||||
title="Files changed"
|
||||
>
|
||||
{/* Full-bleed: cancel the section's inner padding so the file
|
||||
tree + diff grid spans the whole content column, and let it
|
||||
grow to the bottom of the scrollport when the page is short. */}
|
||||
<div className="-mx-6 -mb-6 flex min-h-0 flex-1 flex-col">
|
||||
<ProjectDiffFilesPanel
|
||||
className="min-h-0 flex-1"
|
||||
fileTreeClassName="max-h-none"
|
||||
diff={diff}
|
||||
embedded
|
||||
error={diffError}
|
||||
|
||||
@@ -889,7 +889,10 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
|
||||
className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overflow-y-auto overscroll-y-none px-4 pb-4"
|
||||
data-testid="project-detail-scroll"
|
||||
>
|
||||
<div className="w-full space-y-3">
|
||||
{/* min-h-full + flex chain lets the commit detail's diff pane
|
||||
grow to the bottom of the scrollport without forcing a
|
||||
taller page when content already overflows. */}
|
||||
<div className="flex min-h-full w-full flex-col space-y-3">
|
||||
<WorkspaceTabs
|
||||
key={`${project.id}:${repository.id}:${tabsResetKey}`}
|
||||
initialTab={
|
||||
|
||||
@@ -10,16 +10,25 @@ import { cn } from "@/shared/lib/cn";
|
||||
*/
|
||||
export function ProjectDetailSection({
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
count,
|
||||
defaultOpen = true,
|
||||
headerClassName,
|
||||
onOpenChange,
|
||||
open: openProp,
|
||||
testId,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
/** Extra classes for the outer section, e.g. flex growth in fill layouts. */
|
||||
className?: string;
|
||||
/** Extra classes for the expanded content wrapper. */
|
||||
contentClassName?: string;
|
||||
count?: number;
|
||||
defaultOpen?: boolean;
|
||||
/** Extra classes for the header row, e.g. to align it with a reading column while the content is full-bleed. */
|
||||
headerClassName?: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
open?: boolean;
|
||||
testId?: string;
|
||||
@@ -35,12 +44,16 @@ export function ProjectDetailSection({
|
||||
|
||||
return (
|
||||
<section
|
||||
className={className}
|
||||
data-open={open ? "true" : "false"}
|
||||
data-testid={testId ?? "project-detail-section"}
|
||||
>
|
||||
<button
|
||||
aria-expanded={open}
|
||||
className="flex min-h-10 w-full min-w-0 items-center gap-2 px-6 py-2 text-left text-sm font-medium leading-5 text-foreground transition-colors hover:bg-muted/20"
|
||||
className={cn(
|
||||
"flex min-h-10 w-full min-w-0 items-center gap-2 px-6 py-2 text-left text-sm font-medium leading-5 text-foreground transition-colors hover:bg-muted/20",
|
||||
headerClassName,
|
||||
)}
|
||||
onClick={() => setOpen(!open)}
|
||||
type="button"
|
||||
>
|
||||
@@ -59,7 +72,9 @@ export function ProjectDetailSection({
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
{open ? <div className="px-6 pb-6 pt-1">{children}</div> : null}
|
||||
{open ? (
|
||||
<div className={cn("px-6 pb-6 pt-1", contentClassName)}>{children}</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -755,8 +755,10 @@ export function ProjectPullRequestFilesChangedPanel({
|
||||
}
|
||||
|
||||
export function ProjectDiffFilesPanel({
|
||||
className,
|
||||
error,
|
||||
diff,
|
||||
fileTreeClassName,
|
||||
isLoading,
|
||||
embedded = false,
|
||||
focusedAnchor,
|
||||
@@ -764,6 +766,10 @@ export function ProjectDiffFilesPanel({
|
||||
inlineComments,
|
||||
subjectLabel,
|
||||
}: {
|
||||
/** Extra classes for the file-tree/diff grid container. */
|
||||
className?: string;
|
||||
/** Overrides the file tree's default `max-h-96` cap, e.g. for full-height layouts. */
|
||||
fileTreeClassName?: string;
|
||||
error: unknown;
|
||||
diff: ProjectRepoDiff | null | undefined;
|
||||
isLoading: boolean;
|
||||
@@ -857,6 +863,7 @@ export function ProjectDiffFilesPanel({
|
||||
className={cn(
|
||||
"grid min-h-0 overflow-hidden lg:grid-cols-[17rem_minmax(0,1fr)]",
|
||||
outerBorderClass,
|
||||
className,
|
||||
)}
|
||||
data-project-detail-panel={embedded ? undefined : true}
|
||||
>
|
||||
@@ -876,7 +883,12 @@ export function ProjectDiffFilesPanel({
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<nav className="max-h-96 overflow-auto border-border/50 border-t py-1">
|
||||
<nav
|
||||
className={cn(
|
||||
"max-h-96 overflow-auto border-border/50 border-t py-1",
|
||||
fileTreeClassName,
|
||||
)}
|
||||
>
|
||||
<FileTreeItems
|
||||
node={fileTree}
|
||||
onSelect={setSelectedPath}
|
||||
|
||||
@@ -263,7 +263,7 @@ export function ReadmePanel({
|
||||
return (
|
||||
<section className="overflow-hidden">
|
||||
{header}
|
||||
<div className="p-6 text-sm text-muted-foreground">
|
||||
<div className="px-8 py-6 text-sm text-muted-foreground">
|
||||
{fileContent.error
|
||||
? "Could not load this README. Try again after refreshing the repository."
|
||||
: gitDataState === "empty"
|
||||
@@ -283,11 +283,13 @@ export function ReadmePanel({
|
||||
return (
|
||||
<section className="overflow-hidden">
|
||||
{header}
|
||||
<div className="p-4">
|
||||
<div className="min-w-0 px-8 py-6">
|
||||
{isMarkdown ? (
|
||||
<Markdown
|
||||
blockCode
|
||||
className="text-sm"
|
||||
content={readmeContent}
|
||||
hardLineBreaks={false}
|
||||
interactive={false}
|
||||
/>
|
||||
) : language ? (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BookOpen,
|
||||
CircleDot,
|
||||
Files as FilesIcon,
|
||||
GitCommitHorizontal,
|
||||
@@ -350,9 +349,7 @@ export function WorkspaceTabs({
|
||||
[selectedPullRequestId],
|
||||
);
|
||||
const sectionHeader =
|
||||
selectedTab === "overview" && readmeFile ? (
|
||||
<ProjectSectionHeader icon={BookOpen} title="README" />
|
||||
) : selectedTab === "files" && files.length > 0 ? (
|
||||
selectedTab === "files" && files.length > 0 ? (
|
||||
<ProjectSectionHeader icon={FilesIcon} title="Files" />
|
||||
) : selectedTab === "activity" && !selectedCommitHash ? (
|
||||
<ProjectSectionHeader icon={GitCommitHorizontal} title="Commits" />
|
||||
@@ -387,7 +384,7 @@ export function WorkspaceTabs({
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="min-w-0 space-y-3"
|
||||
className="flex min-w-0 flex-1 flex-col space-y-3"
|
||||
onValueChange={handleTabChange}
|
||||
value={selectedTab}
|
||||
>
|
||||
@@ -422,7 +419,7 @@ export function WorkspaceTabs({
|
||||
{/* Project content follows the same borderless flow as work-item details.
|
||||
Inner panels retain standalone chrome, neutralized here. */}
|
||||
<div
|
||||
className="-mx-4 [&_[data-project-detail-panel]]:rounded-none [&_[data-project-detail-panel]]:border-0"
|
||||
className="-mx-4 flex flex-1 flex-col [&_[data-project-detail-panel]]:rounded-none [&_[data-project-detail-panel]]:border-0"
|
||||
data-testid="project-workspace-panel"
|
||||
>
|
||||
{sectionHeader}
|
||||
@@ -444,7 +441,10 @@ export function WorkspaceTabs({
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent className="m-0" value="activity">
|
||||
<TabsContent
|
||||
className="m-0 min-h-0 flex-1 flex-col data-[state=active]:flex"
|
||||
value="activity"
|
||||
>
|
||||
{repositoryUnavailableState ??
|
||||
(selectedCommitHash ? (
|
||||
<ProjectCommitDetailPanel
|
||||
|
||||
@@ -1259,6 +1259,7 @@ function ImageMosaic({ children }: { children: React.ReactNode[] }) {
|
||||
export function createMarkdownComponents(
|
||||
interactive = true,
|
||||
mediaInset = false,
|
||||
blockCode = false,
|
||||
): Components {
|
||||
const listItemClassName = "[&_p]:inline";
|
||||
const listClassName = "space-y-1 pl-6 marker:text-muted-foreground/80";
|
||||
@@ -1566,7 +1567,7 @@ export function createMarkdownComponents(
|
||||
return <p>{children}</p>;
|
||||
},
|
||||
pre: ({ children }) => {
|
||||
if (!interactive) return <span>{children}</span>;
|
||||
if (!interactive && !blockCode) return <span>{children}</span>;
|
||||
let language = "";
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (
|
||||
@@ -1701,9 +1702,9 @@ export function createMarkdownComponents(
|
||||
}
|
||||
|
||||
/**
|
||||
* The component map only varies by the three boolean render flags, so at most
|
||||
* eight instances ever exist. Module-stable maps mean cached markdown element
|
||||
* trees (see ./markdown/nodeCache.ts) never embed per-mount closures.
|
||||
* The component map only varies by the four boolean render flags, so at most
|
||||
* sixteen instances ever exist. Module-stable maps mean cached markdown
|
||||
* element trees (see ./markdown/nodeCache.ts) never embed per-mount closures.
|
||||
*/
|
||||
const MARKDOWN_COMPONENT_SCHEMA_VERSION = "8";
|
||||
const markdownComponentsByVariant = new Map<string, MarkdownComponentSet>();
|
||||
@@ -1721,12 +1722,13 @@ function getMarkdownComponents(
|
||||
interactive: boolean,
|
||||
leadingInlineContent: boolean,
|
||||
mediaInset: boolean,
|
||||
blockCode: boolean,
|
||||
): MarkdownComponentSet {
|
||||
const variant = `${MARKDOWN_COMPONENT_SCHEMA_VERSION}:${interactive ? "i" : ""}${leadingInlineContent ? "l" : ""}${mediaInset ? "m" : ""}`;
|
||||
const variant = `${MARKDOWN_COMPONENT_SCHEMA_VERSION}:${interactive ? "i" : ""}${leadingInlineContent ? "l" : ""}${mediaInset ? "m" : ""}${blockCode ? "c" : ""}`;
|
||||
let entry = markdownComponentsByVariant.get(variant);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
components: createMarkdownComponents(interactive, mediaInset),
|
||||
components: createMarkdownComponents(interactive, mediaInset, blockCode),
|
||||
variant,
|
||||
};
|
||||
markdownComponentsByVariant.set(variant, entry);
|
||||
@@ -1743,6 +1745,7 @@ function MarkdownInner({
|
||||
hardLineBreaks = true,
|
||||
imetaByUrl,
|
||||
interactive = true,
|
||||
blockCode = false,
|
||||
agentMentionPubkeysByName,
|
||||
leadingInlineContent,
|
||||
mediaInset = false,
|
||||
@@ -1857,6 +1860,7 @@ function MarkdownInner({
|
||||
interactive,
|
||||
hasLeadingInlineContent,
|
||||
mediaInset,
|
||||
blockCode,
|
||||
);
|
||||
const markdownNode =
|
||||
configNudge === null
|
||||
|
||||
@@ -77,6 +77,13 @@ export type MarkdownProps = {
|
||||
hardLineBreaks?: boolean;
|
||||
imetaByUrl?: ImetaLookup;
|
||||
interactive?: boolean;
|
||||
/**
|
||||
* Render fenced code as scrollable code blocks even when `interactive` is
|
||||
* false. Non-interactive surfaces default to inlining code (compact
|
||||
* previews); document surfaces like repository READMEs pass true so long
|
||||
* lines scroll inside the block instead of stretching the layout.
|
||||
*/
|
||||
blockCode?: boolean;
|
||||
agentMentionPubkeysByName?: Record<string, string>;
|
||||
mentionNames?: string[];
|
||||
mentionPubkeysByName?: Record<string, string>;
|
||||
|
||||
@@ -111,6 +111,7 @@ export function markdownPropsAreEqual(
|
||||
prev.customEmoji === next.customEmoji &&
|
||||
prev.hardLineBreaks === next.hardLineBreaks &&
|
||||
prev.interactive === next.interactive &&
|
||||
prev.blockCode === next.blockCode &&
|
||||
prev.mediaInset === next.mediaInset &&
|
||||
shallowRecordEqual(
|
||||
prev.agentMentionPubkeysByName,
|
||||
|
||||
Reference in New Issue
Block a user