Skip to content
Open
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
104 changes: 73 additions & 31 deletions src/browser/components/RightSidebar/CodeReview/UntrackedStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* UntrackedStatus - Shows untracked files count with interactive tooltip
*/

import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/common/lib/utils";

interface UntrackedStatusProps {
Expand All @@ -22,10 +23,40 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
const [isLoading, setIsLoading] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [isTracking, setIsTracking] = useState(false);
const [popupPosition, setPopupPosition] = useState<{ top: number; right: number } | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const hasLoadedOnce = useRef(false);
const loadingRef = useRef(false); // Prevent concurrent loads

// Calculate popup position when shown
useLayoutEffect(() => {
if (!showTooltip || !containerRef.current) {
setPopupPosition(null);
return;
}

const updatePosition = () => {
const container = containerRef.current;
if (!container) return;

const rect = container.getBoundingClientRect();
setPopupPosition({
top: rect.bottom + 8, // 8px gap below anchor
right: window.innerWidth - rect.right,
});
};

updatePosition();

window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}, [showTooltip]);

// Load untracked files
useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -78,7 +109,10 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
if (!showTooltip) return;

const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
const target = e.target as Node;
const clickedContainer = containerRef.current?.contains(target);
const clickedPopup = popupRef.current?.contains(target);
if (!clickedContainer && !clickedPopup) {
setShowTooltip(false);
}
};
Expand Down Expand Up @@ -121,7 +155,7 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
const hasUntracked = count > 0;

return (
<div ref={containerRef} className="relative inline-block">
<div ref={containerRef} className="inline-block">
<div
className={cn(
"py-1 px-2.5 rounded font-medium text-[11px] whitespace-nowrap transition-all duration-200",
Expand All @@ -134,35 +168,43 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
{isLoading ? "..." : `${count} Untracked`}
</div>

{showTooltip && hasUntracked && (
<div className="bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 absolute top-[calc(100%+8px)] right-0 z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150">
<div className="text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold">
Untracked Files ({count})
</div>
<div className="mb-2 max-h-[200px] overflow-y-auto">
{untrackedFiles.map((file) => (
<div
key={file}
className="text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
>
{file}
</div>
))}
</div>
<button
onClick={() => void handleTrackAll()}
disabled={isTracking}
className={cn(
"w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary",
"hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle",
"active:bg-white-overlay",
"disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
)}
{showTooltip &&
hasUntracked &&
popupPosition &&
createPortal(
<div
ref={popupRef}
className="bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 fixed z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150"
style={{ top: popupPosition.top, right: popupPosition.right }}
>
{isTracking ? "Tracking..." : "Track All"}
</button>
</div>
)}
<div className="text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold">
Untracked Files ({count})
</div>
<div className="mb-2 max-h-[200px] overflow-y-auto">
{untrackedFiles.map((file) => (
<div
key={file}
className="text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
>
{file}
</div>
))}
</div>
<button
onClick={() => void handleTrackAll()}
disabled={isTracking}
className={cn(
"w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary",
"hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle",
"active:bg-white-overlay",
"disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
)}
>
{isTracking ? "Tracking..." : "Track All"}
</button>
</div>,
document.body
)}
</div>
);
};