mirror of
https://github.com/better-auth/better-hub
synced 2026-08-09 12:26:04 +00:00
feat: PR diff sidebar has 'marked viewed' checkbox & flat file view (#282)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo, memo } from "react";
|
||||
import { useState, useCallback, useMemo, memo, useEffect, useRef } from "react";
|
||||
import {
|
||||
ChevronRight,
|
||||
FilePlus2,
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
Check,
|
||||
Search,
|
||||
X,
|
||||
FolderTree,
|
||||
List,
|
||||
} from "lucide-react";
|
||||
import { FileTypeIcon } from "@/components/shared/file-icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -28,6 +30,8 @@ interface DiffFileTreeProps {
|
||||
onSelectFile: (index: number) => void;
|
||||
viewedFiles: Set<string>;
|
||||
threadsByFile: Map<string, ReviewThread[]>;
|
||||
onToggleViewed: (filename: string) => void;
|
||||
onSetFilesViewed: (filenames: string[], viewed: boolean) => void;
|
||||
}
|
||||
|
||||
function getFileStatusIcon(status: string) {
|
||||
@@ -85,12 +89,18 @@ function buildSearchIndex(nodes: DiffTreeNode[]): SearchEntry[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
type ViewMode = "tree" | "flat";
|
||||
|
||||
function DiffFileSearchBar({
|
||||
searchIndex,
|
||||
onSelectFile,
|
||||
viewMode,
|
||||
onToggleViewMode,
|
||||
}: {
|
||||
searchIndex: SearchEntry[];
|
||||
onSelectFile: (index: number) => void;
|
||||
viewMode: ViewMode;
|
||||
onToggleViewMode: () => void;
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||
@@ -149,31 +159,48 @@ function DiffFileSearchBar({
|
||||
|
||||
return (
|
||||
<div className="shrink-0 p-2 relative">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground/50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter files..."
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSelectedIdx(0);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full text-[11px] font-mono pl-7 pr-7 py-1.5 bg-transparent border border-border rounded focus:outline-none focus:ring-1 focus:ring-muted-foreground/30 placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
{inputValue && (
|
||||
<button
|
||||
onClick={() => setInputValue("")}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 text-muted-foreground/50 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground/50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter files..."
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSelectedIdx(0);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full text-[11px] font-mono pl-7 pr-7 py-1.5 bg-transparent border border-border rounded focus:outline-none focus:ring-1 focus:ring-muted-foreground/30 placeholder:text-muted-foreground/50"
|
||||
/>
|
||||
{inputValue && (
|
||||
<button
|
||||
onClick={() => setInputValue("")}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 text-muted-foreground/50 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleViewMode}
|
||||
className="p-1.5 rounded border border-border hover:bg-muted/50 transition-colors"
|
||||
title={
|
||||
viewMode === "tree"
|
||||
? "Switch to flat view"
|
||||
: "Switch to tree view"
|
||||
}
|
||||
>
|
||||
{viewMode === "tree" ? (
|
||||
<List className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<FolderTree className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDropdown && (
|
||||
<div className="absolute left-2 right-2 top-full mt-0.5 z-30 max-h-72 overflow-y-auto bg-background border border-border rounded-md shadow-lg">
|
||||
<div className="absolute left-2 right-10 top-full mt-0.5 z-30 max-h-72 overflow-y-auto bg-background border border-border rounded-md shadow-lg">
|
||||
{suggestions.length === 0 ? (
|
||||
<p className="text-[11px] text-muted-foreground/50 font-mono px-3 py-2">
|
||||
No files found
|
||||
@@ -236,6 +263,21 @@ interface TreeNodeProps {
|
||||
threadsByFile: Map<string, ReviewThread[]>;
|
||||
expandedPaths: Set<string>;
|
||||
onToggle: (path: string) => void;
|
||||
onToggleViewed: (filename: string) => void;
|
||||
onSetFilesViewed: (filenames: string[], viewed: boolean) => void;
|
||||
}
|
||||
|
||||
function collectFilePaths(node: DiffTreeNode): string[] {
|
||||
const paths: string[] = [];
|
||||
function walk(n: DiffTreeNode) {
|
||||
if (n.type === "file") {
|
||||
paths.push(n.path);
|
||||
} else if (n.children) {
|
||||
for (const child of n.children) walk(child);
|
||||
}
|
||||
}
|
||||
walk(node);
|
||||
return paths;
|
||||
}
|
||||
|
||||
const DiffTreeNode = memo(function DiffTreeNode({
|
||||
@@ -247,17 +289,25 @@ const DiffTreeNode = memo(function DiffTreeNode({
|
||||
threadsByFile,
|
||||
expandedPaths,
|
||||
onToggle,
|
||||
onToggleViewed,
|
||||
onSetFilesViewed,
|
||||
}: TreeNodeProps) {
|
||||
const isExpanded = expandedPaths.has(node.path);
|
||||
const paddingLeft = depth * 16 + 8;
|
||||
|
||||
if (node.type === "dir") {
|
||||
const filePaths = collectFilePaths(node);
|
||||
const viewedCount = filePaths.filter((p) => viewedFiles.has(p)).length;
|
||||
const allViewed = filePaths.length > 0 && viewedCount === filePaths.length;
|
||||
const someViewed = viewedCount > 0 && viewedCount < filePaths.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
<div
|
||||
onClick={() => onToggle(node.path)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 w-full text-left py-[3px] pr-2 hover:bg-muted/50 dark:hover:bg-white/[0.02] transition-colors group relative",
|
||||
"flex items-center gap-1.5 w-full text-left py-[3px] pr-2 hover:bg-card transition-colors group cursor-pointer relative",
|
||||
allViewed && "opacity-70",
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
>
|
||||
@@ -284,16 +334,46 @@ const DiffTreeNode = memo(function DiffTreeNode({
|
||||
{node.name}
|
||||
</span>
|
||||
{(node.additions ?? 0) > 0 && (
|
||||
<span className="text-[9px] font-mono text-success tabular-nums shrink-0">
|
||||
<span className="text-[9px] font-mono text-success tabular-nums shrink-0 group-hover:hidden">
|
||||
+{node.additions}
|
||||
</span>
|
||||
)}
|
||||
{(node.deletions ?? 0) > 0 && (
|
||||
<span className="text-[9px] font-mono text-destructive tabular-nums shrink-0">
|
||||
<span className="text-[9px] font-mono text-destructive tabular-nums shrink-0 group-hover:hidden">
|
||||
-{node.deletions}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSetFilesViewed(filePaths, !allViewed);
|
||||
}}
|
||||
className="hidden group-hover:flex items-center justify-end w-[34px] shrink-0"
|
||||
title={
|
||||
allViewed
|
||||
? "Mark all as unviewed"
|
||||
: "Mark all as viewed"
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"w-3.5 h-3.5 rounded border flex items-center justify-center transition-colors cursor-pointer",
|
||||
allViewed
|
||||
? "bg-success border-success"
|
||||
: someViewed
|
||||
? "border-success bg-success/30"
|
||||
: "border-muted-foreground/40 hover:border-muted-foreground/60",
|
||||
)}
|
||||
>
|
||||
{allViewed && (
|
||||
<Check className="w-2.5 h-2.5 text-success-foreground" />
|
||||
)}
|
||||
{someViewed && !allViewed && (
|
||||
<span className="w-1.5 h-1.5 bg-success rounded-sm" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid transition-[grid-template-rows] duration-150 ease-out",
|
||||
@@ -312,6 +392,8 @@ const DiffTreeNode = memo(function DiffTreeNode({
|
||||
threadsByFile={threadsByFile}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={onToggle}
|
||||
onToggleViewed={onToggleViewed}
|
||||
onSetFilesViewed={onSetFilesViewed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -326,14 +408,14 @@ const DiffTreeNode = memo(function DiffTreeNode({
|
||||
const Icon = getFileStatusIcon(node.status ?? "modified");
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => node.fileIndex !== undefined && onSelectFile(node.fileIndex)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 w-full text-left py-[3px] pr-2 hover:bg-muted/50 dark:hover:bg-white/[0.02] transition-colors relative group/file cursor-pointer",
|
||||
isActive && "bg-muted/70",
|
||||
isViewed && "opacity-50",
|
||||
"flex items-center gap-1.5 w-full text-left py-[3px] pr-2 hover:bg-card transition-colors relative group/file cursor-pointer",
|
||||
isActive && "bg-accent!",
|
||||
isViewed && "opacity-70",
|
||||
)}
|
||||
style={{ paddingLeft: paddingLeft + 15 }}
|
||||
onClick={() => node.fileIndex !== undefined && onSelectFile(node.fileIndex)}
|
||||
>
|
||||
{Array.from({ length: depth }).map((_, i) => (
|
||||
<span
|
||||
@@ -371,13 +453,158 @@ const DiffTreeNode = memo(function DiffTreeNode({
|
||||
title={`${fileThreads.length} review thread${fileThreads.length !== 1 ? "s" : ""}`}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[9px] font-mono text-success tabular-nums shrink-0">
|
||||
<span className="text-[9px] font-mono text-success tabular-nums shrink-0 group-hover/file:hidden">
|
||||
+{node.additions ?? 0}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono text-destructive tabular-nums shrink-0">
|
||||
<span className="text-[9px] font-mono text-destructive tabular-nums shrink-0 group-hover/file:hidden">
|
||||
-{node.deletions ?? 0}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleViewed(node.path);
|
||||
}}
|
||||
className="hidden group-hover/file:flex items-center justify-end w-[34px] shrink-0"
|
||||
title={isViewed ? "Mark as unviewed" : "Mark as viewed"}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"w-3.5 h-3.5 rounded border flex items-center justify-center transition-colors cursor-pointer",
|
||||
isViewed
|
||||
? "bg-success border-success"
|
||||
: "border-muted-foreground/40 hover:border-muted-foreground/60",
|
||||
)}
|
||||
>
|
||||
{isViewed && (
|
||||
<Check className="w-2.5 h-2.5 text-success-foreground" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function collectAllDirPaths(nodes: DiffTreeNode[]): string[] {
|
||||
const paths: string[] = [];
|
||||
function walk(list: DiffTreeNode[]) {
|
||||
for (const n of list) {
|
||||
if (n.type === "dir") {
|
||||
paths.push(n.path);
|
||||
if (n.children) walk(n.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(nodes);
|
||||
return paths;
|
||||
}
|
||||
|
||||
interface FlatFileItemProps {
|
||||
file: DiffFile;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
isViewed: boolean;
|
||||
threads: ReviewThread[] | undefined;
|
||||
onSelectFile: (index: number) => void;
|
||||
onToggleViewed: (filename: string) => void;
|
||||
}
|
||||
|
||||
const FlatFileItem = memo(function FlatFileItem({
|
||||
file,
|
||||
index,
|
||||
isActive,
|
||||
isViewed,
|
||||
threads,
|
||||
onSelectFile,
|
||||
onToggleViewed,
|
||||
}: FlatFileItemProps) {
|
||||
const Icon = getFileStatusIcon(file.status);
|
||||
const lastSlash = file.filename.lastIndexOf("/");
|
||||
const dirPath = lastSlash > 0 ? file.filename.slice(0, lastSlash + 1) : "";
|
||||
const fileName = lastSlash > 0 ? file.filename.slice(lastSlash + 1) : file.filename;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelectFile(index)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 w-full text-left py-[3px] px-2 hover:bg-card transition-colors relative group/file cursor-pointer",
|
||||
isActive && "bg-accent",
|
||||
isViewed && "opacity-70",
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<span className="absolute left-0 top-0 bottom-0 w-0.5 bg-foreground" />
|
||||
)}
|
||||
{isViewed ? (
|
||||
<Check className="w-3 h-3 shrink-0 text-success" />
|
||||
) : (
|
||||
<Icon
|
||||
className={cn(
|
||||
"w-3 h-3 shrink-0",
|
||||
getFileStatusColor(file.status),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="font-mono flex items-baseline min-w-0 flex-1 group-hover/file:text-foreground"
|
||||
title={file.filename}
|
||||
>
|
||||
{dirPath && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-[9px] truncate min-w-0 shrink",
|
||||
isViewed
|
||||
? "text-muted-foreground/40 line-through"
|
||||
: "text-muted-foreground/50",
|
||||
)}
|
||||
>
|
||||
{dirPath}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"text-[11px] shrink-0",
|
||||
isViewed
|
||||
? "text-muted-foreground/60 line-through"
|
||||
: "text-foreground/80",
|
||||
)}
|
||||
>
|
||||
{fileName}
|
||||
</span>
|
||||
</span>
|
||||
{threads && threads.length > 0 && (
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full bg-warning/60 shrink-0"
|
||||
title={`${threads.length} review thread${threads.length !== 1 ? "s" : ""}`}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[9px] font-mono text-success tabular-nums shrink-0 group-hover/file:hidden">
|
||||
+{file.additions}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono text-destructive tabular-nums shrink-0 group-hover/file:hidden">
|
||||
-{file.deletions}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleViewed(file.filename);
|
||||
}}
|
||||
className="hidden group-hover/file:flex items-center justify-end w-[34px] shrink-0"
|
||||
title={isViewed ? "Mark as unviewed" : "Mark as viewed"}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"w-3.5 h-3.5 rounded border flex items-center justify-center transition-colors cursor-pointer",
|
||||
isViewed
|
||||
? "bg-success border-success"
|
||||
: "border-muted-foreground/40 hover:border-muted-foreground/60",
|
||||
)}
|
||||
>
|
||||
{isViewed && (
|
||||
<Check className="w-2.5 h-2.5 text-success-foreground" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -387,18 +614,24 @@ export function DiffFileTree({
|
||||
onSelectFile,
|
||||
viewedFiles,
|
||||
threadsByFile,
|
||||
onToggleViewed,
|
||||
onSetFilesViewed,
|
||||
}: DiffFileTreeProps) {
|
||||
const tree = useMemo(() => buildDiffFileTree(files), [files]);
|
||||
const searchIndex = useMemo(() => buildSearchIndex(tree), [tree]);
|
||||
|
||||
const currentFile = files[activeIndex];
|
||||
const currentPath = currentFile?.filename ?? null;
|
||||
const allDirPaths = useMemo(() => collectAllDirPaths(tree), [tree]);
|
||||
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => {
|
||||
if (!currentPath) return new Set<string>();
|
||||
const ancestors = getAncestorPaths(currentPath);
|
||||
return new Set(ancestors);
|
||||
});
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set<string>());
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("tree");
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!initializedRef.current && allDirPaths.length > 0) {
|
||||
initializedRef.current = true;
|
||||
setExpandedPaths(new Set(allDirPaths));
|
||||
}
|
||||
}, [allDirPaths]);
|
||||
|
||||
const toggleExpand = useCallback((path: string) => {
|
||||
setExpandedPaths((prev) => {
|
||||
@@ -409,6 +642,10 @@ export function DiffFileTree({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleViewMode = useCallback(() => {
|
||||
setViewMode((prev) => (prev === "tree" ? "flat" : "tree"));
|
||||
}, []);
|
||||
|
||||
const handleSelectFile = useCallback(
|
||||
(index: number) => {
|
||||
const file = files[index];
|
||||
@@ -430,21 +667,61 @@ export function DiffFileTree({
|
||||
<DiffFileSearchBar
|
||||
searchIndex={searchIndex}
|
||||
onSelectFile={handleSelectFile}
|
||||
viewMode={viewMode}
|
||||
onToggleViewMode={toggleViewMode}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden py-1">
|
||||
{tree.map((node) => (
|
||||
<DiffTreeNode
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeIndex={activeIndex}
|
||||
onSelectFile={handleSelectFile}
|
||||
viewedFiles={viewedFiles}
|
||||
threadsByFile={threadsByFile}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={toggleExpand}
|
||||
/>
|
||||
))}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden py-1 pr-2 pb-12">
|
||||
<div key={viewMode} className="animate-in fade-in duration-150">
|
||||
{viewMode === "tree"
|
||||
? tree.map((node) => (
|
||||
<DiffTreeNode
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeIndex={activeIndex}
|
||||
onSelectFile={
|
||||
handleSelectFile
|
||||
}
|
||||
viewedFiles={viewedFiles}
|
||||
threadsByFile={
|
||||
threadsByFile
|
||||
}
|
||||
expandedPaths={
|
||||
expandedPaths
|
||||
}
|
||||
onToggle={toggleExpand}
|
||||
onToggleViewed={
|
||||
onToggleViewed
|
||||
}
|
||||
onSetFilesViewed={
|
||||
onSetFilesViewed
|
||||
}
|
||||
/>
|
||||
))
|
||||
: files.map((file, index) => (
|
||||
<FlatFileItem
|
||||
key={file.filename}
|
||||
file={file}
|
||||
index={index}
|
||||
isActive={
|
||||
index ===
|
||||
activeIndex
|
||||
}
|
||||
isViewed={viewedFiles.has(
|
||||
file.filename,
|
||||
)}
|
||||
threads={threadsByFile.get(
|
||||
file.filename,
|
||||
)}
|
||||
onSelectFile={
|
||||
handleSelectFile
|
||||
}
|
||||
onToggleViewed={
|
||||
onToggleViewed
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -237,6 +237,17 @@ export function PRDiffViewer({
|
||||
});
|
||||
};
|
||||
|
||||
const setFilesViewed = (filenames: string[], viewed: boolean) => {
|
||||
setViewedFiles((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const f of filenames) {
|
||||
if (viewed) next.add(f);
|
||||
else next.delete(f);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const viewedCount = viewedFiles.size;
|
||||
|
||||
// Group review comments by file
|
||||
@@ -261,7 +272,7 @@ export function PRDiffViewer({
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
setSidebarWidth(Math.max(140, Math.min(400, x)));
|
||||
setSidebarWidth(Math.max(140, Math.min(600, x)));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -270,7 +281,7 @@ export function PRDiffViewer({
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<div
|
||||
className="hidden lg:flex flex-col shrink-0 border-r border-border pr-3"
|
||||
className="hidden lg:flex flex-col shrink-0 border-r border-border pr-2"
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
transition: isDragging
|
||||
@@ -364,8 +375,15 @@ export function PRDiffViewer({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewedCount > 0 && (
|
||||
<div className="shrink-0 h-1 bg-border/60 mx-3 rounded-full overflow-hidden">
|
||||
{
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 h-1 mx-3 rounded-full overflow-hidden transition-all duration-300",
|
||||
viewedCount === 0
|
||||
? "bg-border/20"
|
||||
: "bg-border/60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-success/70 transition-all duration-300 rounded-full"
|
||||
style={{
|
||||
@@ -373,7 +391,7 @@ export function PRDiffViewer({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
|
||||
{/* Sidebar content */}
|
||||
<div className="flex-1 overflow-y-auto overscroll-contain min-h-0">
|
||||
@@ -388,6 +406,12 @@ export function PRDiffViewer({
|
||||
threadsByFile={
|
||||
threadsByFile
|
||||
}
|
||||
onToggleViewed={
|
||||
toggleViewed
|
||||
}
|
||||
onSetFilesViewed={
|
||||
setFilesViewed
|
||||
}
|
||||
/>
|
||||
) : sidebarMode === "commits" ? (
|
||||
<SidebarCommits
|
||||
|
||||
Reference in New Issue
Block a user