Skip to content
Open
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
62 changes: 36 additions & 26 deletions packages/web/src/components/chapter/chapter-file-list.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,55 @@
import {
FILE_VIEWED_STATE,
type FileViewedState,
FileViewRow,
} from "@/components/chapter/file-view-row";
import type { FileDiffEntry } from "@/lib/parse-diff";
import { useMemo, useState } from "react";
import { FileFilterInput } from "@/components/files/file-filter-input";
import { FileTree, type ViewedConfig } from "@/components/files/file-tree";
import { FILE_VIEWED_STATE, type PullRequestFile } from "@/lib/diff-types";

// The CLI has no review comments, so the tree never renders comment badges.
const NO_COMMENT_COUNTS: Map<string, number> = new Map();

interface ChapterFileListProps {
entries: FileDiffEntry[];
files: PullRequestFile[];
focusedFilePath?: string;
viewedPathSet: ReadonlySet<string>;
onToggleFileViewed: (filePath: string) => void;
onSelectFile: (filePath: string) => void;
}

export function ChapterFileList({
entries,
files,
focusedFilePath,
viewedPathSet,
onToggleFileViewed,
onSelectFile,
}: ChapterFileListProps) {
const [filter, setFilter] = useState("");

const viewed = useMemo<ViewedConfig>(
() => ({
stateByPath: new Map(
files.map((file) => [
file.path,
viewedPathSet.has(file.path) ? FILE_VIEWED_STATE.VIEWED : FILE_VIEWED_STATE.UNVIEWED,
]),
),
onToggle: onToggleFileViewed,
}),
[files, viewedPathSet, onToggleFileViewed],
);

return (
<div className="py-3 pl-6 pr-4 lg:pl-8">
<h2 className="mb-2 font-medium text-[11px] text-muted-foreground uppercase tracking-wider">
Files <span className="text-muted-foreground/60">({entries.length})</span>
Files <span className="text-muted-foreground/60">({files.length})</span>
</h2>
<div className="space-y-0.5">
{entries.map(({ file }) => {
const viewedState: FileViewedState = viewedPathSet.has(file.path)
? FILE_VIEWED_STATE.VIEWED
: FILE_VIEWED_STATE.UNVIEWED;
return (
<FileViewRow
key={file.path}
filePath={file.path}
status={file.status}
viewedState={viewedState}
onToggleViewed={onToggleFileViewed}
onSelect={onSelectFile}
/>
);
})}
</div>
<FileFilterInput value={filter} onChange={setFilter} className="mb-2" />
<FileTree
files={files}
focusedFilePath={focusedFilePath}
onSelectFile={onSelectFile}
viewed={viewed}
commentCountsByPath={NO_COMMENT_COUNTS}
filter={filter}
/>
</div>
);
}
12 changes: 12 additions & 0 deletions packages/web/src/components/chapter/chapter-panel-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Shared resize bounds for the chapter side panel. Width is viewport-relative:
* a 30% default capped at 50%, with a fixed pixel floor.
*/
export const CHAPTER_PANEL_MIN_WIDTH = 280;
export const CHAPTER_PANEL_MAX_WIDTH_FRACTION = 0.5;

export const resolveChapterPanelDefaultWidth = (viewportWidth: number) =>
Math.round(viewportWidth * 0.3);

export const resolveChapterPanelMaxWidth = (viewportWidth: number) =>
Math.round(viewportWidth * CHAPTER_PANEL_MAX_WIDTH_FRACTION);
82 changes: 26 additions & 56 deletions packages/web/src/components/chapter/chapter-side-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import type { Chapter } from "@stagereview/types/chapters";
import { useCallback, useEffect, useRef, useState } from "react";
import { LineCounts } from "@/components/shared/line-counts";
import { Markdown } from "@/components/ui/markdown";
import { useChapterContext } from "@/lib/chapter-context";
import type { FileDiffEntry } from "@/lib/parse-diff";
import type { PullRequestFile } from "@/lib/diff-types";
import { useResizablePanel } from "@/lib/use-resizable-panel";
import { ChapterFileList } from "./chapter-file-list";
import { ChapterNavigator } from "./chapter-navigator";
import {
CHAPTER_PANEL_MAX_WIDTH_FRACTION,
CHAPTER_PANEL_MIN_WIDTH,
resolveChapterPanelDefaultWidth,
resolveChapterPanelMaxWidth,
} from "./chapter-panel-constants";
import { ChapterSummary } from "./chapter-summary";

const MIN_WIDTH = 280;
const DEFAULT_WIDTH_FRACTION = 0.3;
const MAX_WIDTH_FRACTION = 0.5;
const SSR_FALLBACK_WIDTH = Math.round(1440 * DEFAULT_WIDTH_FRACTION);

interface ChapterSidePanelProps {
chapter: Chapter;
chapterIndex: number;
chapterEntries: FileDiffEntry[];
files: PullRequestFile[];
focusedFilePath?: string;
viewedChapterIds: ReadonlySet<string>;
checkedKeyChangeIds: ReadonlySet<string>;
viewedFilePathSet: ReadonlySet<string>;
Expand All @@ -32,7 +34,8 @@ interface ChapterSidePanelProps {
export function ChapterSidePanel({
chapter,
chapterIndex,
chapterEntries,
files,
focusedFilePath,
viewedChapterIds,
checkedKeyChangeIds,
viewedFilePathSet,
Expand All @@ -46,54 +49,22 @@ export function ChapterSidePanel({
}: ChapterSidePanelProps) {
const { chapterLineCountsMap } = useChapterContext();
const lineCounts = chapterLineCountsMap.get(chapter.id);
const [width, setWidth] = useState(SSR_FALLBACK_WIDTH);
const cleanupRef = useRef<(() => void) | null>(null);

useEffect(() => {
const max = Math.round(window.innerWidth * MAX_WIDTH_FRACTION);
const def = Math.round(window.innerWidth * DEFAULT_WIDTH_FRACTION);
setWidth(Math.min(max, Math.max(MIN_WIDTH, def)));
}, []);

const handleDoubleClick = useCallback(() => {
const max = Math.round(window.innerWidth * MAX_WIDTH_FRACTION);
const def = Math.round(window.innerWidth * DEFAULT_WIDTH_FRACTION);
setWidth(Math.min(max, Math.max(MIN_WIDTH, def)));
}, []);

const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = width;

const onMove = (ev: MouseEvent) => {
const max = Math.round(window.innerWidth * MAX_WIDTH_FRACTION);
setWidth(Math.min(max, Math.max(MIN_WIDTH, startWidth + ev.clientX - startX)));
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
cleanupRef.current = null;
};

document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
cleanupRef.current = onUp;
},
[width],
);

useEffect(() => () => cleanupRef.current?.(), []);
const { width, panelRef, resizeHandleProps } = useResizablePanel({
minWidth: CHAPTER_PANEL_MIN_WIDTH,
maxWidth: resolveChapterPanelMaxWidth,
defaultWidth: resolveChapterPanelDefaultWidth,
});

return (
<div
ref={panelRef}
className="sticky top-[var(--content-top)] flex h-[calc(100vh_-_var(--content-top))] flex-col border-border border-r bg-card/30"
style={{ width, minWidth: MIN_WIDTH, maxWidth: `${MAX_WIDTH_FRACTION * 100}vw` }}
style={{
width,
minWidth: CHAPTER_PANEL_MIN_WIDTH,
maxWidth: `${CHAPTER_PANEL_MAX_WIDTH_FRACTION * 100}vw`,
}}
>
<div className="shrink-0 border-border border-b">
<ChapterNavigator
Expand Down Expand Up @@ -128,17 +99,16 @@ export function ChapterSidePanel({
/>
<div className="border-border border-t">
<ChapterFileList
entries={chapterEntries}
files={files}
focusedFilePath={focusedFilePath}
viewedPathSet={viewedFilePathSet}
onToggleFileViewed={onToggleFileViewed}
onSelectFile={onSelectFile}
/>
</div>
</div>
{/* biome-ignore lint/a11y/noStaticElementInteractions: resize handle is a drag target, not an interactive widget */}
<div
onDoubleClick={handleDoubleClick}
onMouseDown={handleMouseDown}
{...resizeHandleProps}
className="absolute top-0 right-0 z-10 h-full w-1 cursor-col-resize hover:bg-primary/30 active:bg-primary/50"
/>
</div>
Expand Down
116 changes: 0 additions & 116 deletions packages/web/src/components/chapter/file-view-row.tsx

This file was deleted.

1 change: 0 additions & 1 deletion packages/web/src/components/chapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export { ChapterNavigator } from "./chapter-navigator";
export { ChapterSidePanel } from "./chapter-side-panel";
export { ChapterSummary } from "./chapter-summary";
export { FileHeader } from "./file-header";
export { FILE_VIEWED_STATE, type FileViewedState, FileViewRow } from "./file-view-row";
export {
findKeyChangeIdAtPoint,
isPointInReviewStateBadge,
Expand Down
Loading
Loading