Move scroll-to-bottom button to issue detail and run pages
Removed the scroll-to-bottom button from IssuesList (wrong location) and created a shared ScrollToBottom component. Added it to IssueDetail and RunDetail pages. On mobile, the button sits above the bottom nav to avoid overlap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||||
import { CircleDot, Plus, Filter, ArrowUpDown, Layers, Check, X, ChevronRight, List, Columns3, User, Search, ArrowDown } from "lucide-react";
|
import { CircleDot, Plus, Filter, ArrowUpDown, Layers, Check, X, ChevronRight, List, Columns3, User, Search } from "lucide-react";
|
||||||
import { KanbanBoard } from "./KanbanBoard";
|
import { KanbanBoard } from "./KanbanBoard";
|
||||||
import type { Issue } from "@paperclipai/shared";
|
import type { Issue } from "@paperclipai/shared";
|
||||||
|
|
||||||
@@ -234,24 +234,6 @@ export function IssuesList({
|
|||||||
|
|
||||||
const activeFilterCount = countActiveFilters(viewState);
|
const activeFilterCount = countActiveFilters(viewState);
|
||||||
|
|
||||||
const [showScrollBottom, setShowScrollBottom] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const el = document.getElementById("main-content");
|
|
||||||
if (!el) return;
|
|
||||||
const check = () => {
|
|
||||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
||||||
setShowScrollBottom(distanceFromBottom > 300);
|
|
||||||
};
|
|
||||||
check();
|
|
||||||
el.addEventListener("scroll", check, { passive: true });
|
|
||||||
return () => el.removeEventListener("scroll", check);
|
|
||||||
}, [filtered.length]);
|
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
|
||||||
const el = document.getElementById("main-content");
|
|
||||||
if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const groupedContent = useMemo(() => {
|
const groupedContent = useMemo(() => {
|
||||||
if (viewState.groupBy === "none") {
|
if (viewState.groupBy === "none") {
|
||||||
return [{ key: "__all", label: null as string | null, items: filtered }];
|
return [{ key: "__all", label: null as string | null, items: filtered }];
|
||||||
@@ -755,15 +737,6 @@ export function IssuesList({
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
{showScrollBottom && (
|
|
||||||
<button
|
|
||||||
onClick={scrollToBottom}
|
|
||||||
className="fixed bottom-6 right-6 z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-colors"
|
|
||||||
aria-label="Scroll to bottom"
|
|
||||||
>
|
|
||||||
<ArrowDown className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
ui/src/components/ScrollToBottom.tsx
Normal file
40
ui/src/components/ScrollToBottom.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { ArrowDown } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floating scroll-to-bottom button that appears when the user is far from the
|
||||||
|
* bottom of the `#main-content` scroll container. Hides when within 300px of
|
||||||
|
* the bottom. Positioned to avoid the mobile bottom nav.
|
||||||
|
*/
|
||||||
|
export function ScrollToBottom() {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = document.getElementById("main-content");
|
||||||
|
if (!el) return;
|
||||||
|
const check = () => {
|
||||||
|
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||||
|
setVisible(distance > 300);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
el.addEventListener("scroll", check, { passive: true });
|
||||||
|
return () => el.removeEventListener("scroll", check);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scroll = useCallback(() => {
|
||||||
|
const el = document.getElementById("main-content");
|
||||||
|
if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={scroll}
|
||||||
|
className="fixed bottom-[calc(1.5rem+5rem+env(safe-area-inset-bottom))] right-6 z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-colors md:bottom-6"
|
||||||
|
aria-label="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import { CopyText } from "../components/CopyText";
|
|||||||
import { EntityRow } from "../components/EntityRow";
|
import { EntityRow } from "../components/EntityRow";
|
||||||
import { Identity } from "../components/Identity";
|
import { Identity } from "../components/Identity";
|
||||||
import { PageSkeleton } from "../components/PageSkeleton";
|
import { PageSkeleton } from "../components/PageSkeleton";
|
||||||
|
import { ScrollToBottom } from "../components/ScrollToBottom";
|
||||||
import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils";
|
import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -1747,6 +1748,7 @@ function RunDetail({ run, agentRouteId, adapterType }: { run: HeartbeatRun; agen
|
|||||||
|
|
||||||
{/* Log viewer */}
|
{/* Log viewer */}
|
||||||
<LogViewer run={run} adapterType={adapterType} />
|
<LogViewer run={run} adapterType={adapterType} />
|
||||||
|
<ScrollToBottom />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { CommentThread } from "../components/CommentThread";
|
|||||||
import { IssueProperties } from "../components/IssueProperties";
|
import { IssueProperties } from "../components/IssueProperties";
|
||||||
import { LiveRunWidget } from "../components/LiveRunWidget";
|
import { LiveRunWidget } from "../components/LiveRunWidget";
|
||||||
import type { MentionOption } from "../components/MarkdownEditor";
|
import type { MentionOption } from "../components/MarkdownEditor";
|
||||||
|
import { ScrollToBottom } from "../components/ScrollToBottom";
|
||||||
import { StatusIcon } from "../components/StatusIcon";
|
import { StatusIcon } from "../components/StatusIcon";
|
||||||
import { PriorityIcon } from "../components/PriorityIcon";
|
import { PriorityIcon } from "../components/PriorityIcon";
|
||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
@@ -926,6 +927,7 @@ export function IssueDetail() {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
<ScrollToBottom />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user