Skip to content

Commit c27164f

Browse files
committed
Split review UI into focused components
1 parent c5366fe commit c27164f

5 files changed

Lines changed: 144 additions & 131 deletions

File tree

src/ui/review-file-body.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { FileDiffOptions } from "@pierre/diffs";
2+
import { FileDiff } from "@pierre/diffs/react";
3+
import type { ReviewFileEntry } from "./review-model.js";
4+
import { StatusLine } from "./review-status-line.js";
5+
6+
export function ReviewFileBody({
7+
entry,
8+
options,
9+
}: {
10+
entry: ReviewFileEntry;
11+
options: FileDiffOptions<undefined>;
12+
}) {
13+
if (!entry.fileDiff) {
14+
return (
15+
<StatusLine message="This file changed without a textual diff that can be rendered." />
16+
);
17+
}
18+
19+
return <FileDiff fileDiff={entry.fileDiff} options={options} className="pierre-diff" />;
20+
}

src/ui/review-file-tree.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useMemo } from "react";
2+
import type { GitStatusEntry } from "@pierre/trees";
3+
import { FileTree, useFileTree } from "@pierre/trees/react";
4+
import type { ReviewFileEntry } from "./review-model.js";
5+
6+
export function ReviewFileTree({
7+
entries,
8+
selectedPath,
9+
onSelect,
10+
}: {
11+
entries: ReviewFileEntry[];
12+
selectedPath?: string;
13+
onSelect(path: string): void;
14+
}) {
15+
const paths = useMemo(() => entries.map((entry) => entry.path), [entries]);
16+
const pathSet = useMemo(() => new Set(paths), [paths]);
17+
const gitStatus = useMemo<GitStatusEntry[]>(
18+
() => entries.map((entry) => ({ path: entry.path, status: entry.status })),
19+
[entries],
20+
);
21+
const { model } = useFileTree({
22+
paths,
23+
gitStatus,
24+
flattenEmptyDirectories: true,
25+
initialExpansion: "open",
26+
search: paths.length > 8,
27+
onSelectionChange(selectedPaths) {
28+
const path = selectedPaths.find((candidate) => pathSet.has(candidate));
29+
if (path) onSelect(path);
30+
},
31+
});
32+
33+
useEffect(() => {
34+
if (!selectedPath) return;
35+
for (const path of model.getSelectedPaths()) {
36+
if (path !== selectedPath) model.getItem(path)?.deselect();
37+
}
38+
model.getItem(selectedPath)?.select();
39+
model.scrollToPath(selectedPath, { focus: false });
40+
}, [model, selectedPath]);
41+
42+
return <FileTree model={model} className="review-file-tree" />;
43+
}

src/ui/review-fullscreen.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect, useState } from "react";
2+
import type { FileDiffOptions } from "@pierre/diffs";
3+
import { ReviewFileBody } from "./review-file-body.js";
4+
import { ReviewFileTree } from "./review-file-tree.js";
5+
import {
6+
initialReviewPath,
7+
type ReviewFileEntry,
8+
} from "./review-model.js";
9+
import { StatusLine } from "./review-status-line.js";
10+
11+
export function FullscreenReview({
12+
entries,
13+
options,
14+
}: {
15+
entries: ReviewFileEntry[];
16+
options: FileDiffOptions<undefined>;
17+
}) {
18+
const [selectedPath, setSelectedPath] = useState(() => initialReviewPath(entries));
19+
20+
useEffect(() => {
21+
setSelectedPath((currentPath) => initialReviewPath(entries, currentPath));
22+
}, [entries]);
23+
24+
const selectedEntry = entries.find((entry) => entry.path === selectedPath) ?? entries[0];
25+
26+
return (
27+
<div className="review-workspace">
28+
<section className="review-selected-file">
29+
<header className="review-selected-file-header">
30+
<div className="review-selected-file-title">
31+
<strong title={selectedEntry?.path}>{selectedEntry?.path}</strong>
32+
{selectedEntry?.previousPath && selectedEntry.previousPath !== selectedEntry.path ? (
33+
<span title={selectedEntry.previousPath}>from {selectedEntry.previousPath}</span>
34+
) : null}
35+
</div>
36+
{selectedEntry ? (
37+
<span className="review-diff-file-stats" aria-label="Selected file diff statistics">
38+
<span className="add">+{selectedEntry.additions}</span>
39+
<span className="remove">-{selectedEntry.removals}</span>
40+
</span>
41+
) : null}
42+
</header>
43+
<div className="review-selected-file-body">
44+
{selectedEntry ? (
45+
<ReviewFileBody entry={selectedEntry} options={options} />
46+
) : (
47+
<StatusLine message="Select a changed file to review it." />
48+
)}
49+
</div>
50+
</section>
51+
52+
<aside className="review-file-tree-panel" aria-label="Changed files">
53+
<div className="review-file-tree-header">
54+
<strong>Changed files</strong>
55+
<span>{entries.length}</span>
56+
</div>
57+
<div className="review-file-tree-body">
58+
<ReviewFileTree
59+
entries={entries}
60+
selectedPath={selectedEntry?.path}
61+
onSelect={setSelectedPath}
62+
/>
63+
</div>
64+
</aside>
65+
</div>
66+
);
67+
}

src/ui/review-payload.tsx

Lines changed: 5 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useMemo, useState } from "react";
22
import { createRoot } from "react-dom/client";
33
import type { FileDiffOptions } from "@pierre/diffs";
4-
import { FileDiff } from "@pierre/diffs/react";
5-
import type { GitStatusEntry } from "@pierre/trees";
6-
import { FileTree, useFileTree } from "@pierre/trees/react";
74
import type { HostContext, ToolResultCard } from "./card-types.js";
8-
import {
9-
buildReviewFileEntries,
10-
initialReviewPath,
11-
type ReviewFileEntry,
12-
} from "./review-model.js";
5+
import { ReviewFileBody } from "./review-file-body.js";
6+
import { FullscreenReview } from "./review-fullscreen.js";
7+
import { buildReviewFileEntries } from "./review-model.js";
8+
import { StatusLine } from "./review-status-line.js";
139

1410
type ThemeType = "light" | "dark";
1511

@@ -105,118 +101,6 @@ function ReviewPayload({
105101
);
106102
}
107103

108-
function FullscreenReview({
109-
entries,
110-
options,
111-
}: {
112-
entries: ReviewFileEntry[];
113-
options: FileDiffOptions<undefined>;
114-
}) {
115-
const [selectedPath, setSelectedPath] = useState(() => initialReviewPath(entries));
116-
117-
useEffect(() => {
118-
setSelectedPath((currentPath) => initialReviewPath(entries, currentPath));
119-
}, [entries]);
120-
121-
const selectedEntry = entries.find((entry) => entry.path === selectedPath) ?? entries[0];
122-
123-
return (
124-
<div className="review-workspace">
125-
<section className="review-selected-file">
126-
<header className="review-selected-file-header">
127-
<div className="review-selected-file-title">
128-
<strong title={selectedEntry?.path}>{selectedEntry?.path}</strong>
129-
{selectedEntry?.previousPath && selectedEntry.previousPath !== selectedEntry.path ? (
130-
<span title={selectedEntry.previousPath}>from {selectedEntry.previousPath}</span>
131-
) : null}
132-
</div>
133-
{selectedEntry ? (
134-
<span className="review-diff-file-stats" aria-label="Selected file diff statistics">
135-
<span className="add">+{selectedEntry.additions}</span>
136-
<span className="remove">-{selectedEntry.removals}</span>
137-
</span>
138-
) : null}
139-
</header>
140-
<div className="review-selected-file-body">
141-
{selectedEntry ? (
142-
<ReviewFileBody entry={selectedEntry} options={options} />
143-
) : (
144-
<StatusLine message="Select a changed file to review it." />
145-
)}
146-
</div>
147-
</section>
148-
149-
<aside className="review-file-tree-panel" aria-label="Changed files">
150-
<div className="review-file-tree-header">
151-
<strong>Changed files</strong>
152-
<span>{entries.length}</span>
153-
</div>
154-
<div className="review-file-tree-body">
155-
<ReviewFileTree
156-
entries={entries}
157-
selectedPath={selectedEntry?.path}
158-
onSelect={setSelectedPath}
159-
/>
160-
</div>
161-
</aside>
162-
</div>
163-
);
164-
}
165-
166-
function ReviewFileTree({
167-
entries,
168-
selectedPath,
169-
onSelect,
170-
}: {
171-
entries: ReviewFileEntry[];
172-
selectedPath?: string;
173-
onSelect(path: string): void;
174-
}) {
175-
const paths = useMemo(() => entries.map((entry) => entry.path), [entries]);
176-
const gitStatus = useMemo<GitStatusEntry[]>(
177-
() => entries.map((entry) => ({ path: entry.path, status: entry.status })),
178-
[entries],
179-
);
180-
const { model } = useFileTree({
181-
paths,
182-
gitStatus,
183-
flattenEmptyDirectories: true,
184-
initialExpansion: "open",
185-
search: paths.length > 8,
186-
onSelectionChange(selectedPaths) {
187-
const path = selectedPaths.find((candidate) => paths.includes(candidate));
188-
if (path) onSelect(path);
189-
},
190-
});
191-
192-
useEffect(() => {
193-
if (!selectedPath) return;
194-
for (const path of model.getSelectedPaths()) {
195-
if (path !== selectedPath) model.getItem(path)?.deselect();
196-
}
197-
model.getItem(selectedPath)?.select();
198-
model.scrollToPath(selectedPath, { focus: false });
199-
}, [model, selectedPath]);
200-
201-
return <FileTree model={model} className="review-file-tree" />;
202-
}
203-
204-
function ReviewFileBody({
205-
entry,
206-
options,
207-
}: {
208-
entry: ReviewFileEntry;
209-
options: FileDiffOptions<undefined>;
210-
}) {
211-
if (!entry.fileDiff) {
212-
return (
213-
<StatusLine message="This file changed without a textual diff that can be rendered." />
214-
);
215-
}
216-
217-
return <FileDiff fileDiff={entry.fileDiff} options={options} className="pierre-diff" />;
218-
}
219-
220104
function diffOptions(themeType: ThemeType): FileDiffOptions<undefined> {
221105
return {
222106
theme: {
@@ -235,13 +119,3 @@ function diffOptions(themeType: ThemeType): FileDiffOptions<undefined> {
235119
disableFileHeader: true,
236120
};
237121
}
238-
239-
function StatusLine({
240-
message,
241-
tone = "muted",
242-
}: {
243-
message: string;
244-
tone?: "muted" | "error";
245-
}) {
246-
return <div className={`status ${tone}`}>{message}</div>;
247-
}

src/ui/review-status-line.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function StatusLine({
2+
message,
3+
tone = "muted",
4+
}: {
5+
message: string;
6+
tone?: "muted" | "error";
7+
}) {
8+
return <div className={`status ${tone}`}>{message}</div>;
9+
}

0 commit comments

Comments
 (0)