fix(ui): hand over to the compact toolbar at lg, and let lists size themselves

All three list pages switched from the compact toolbar to the full one at
sm (640px), so a tablet in portrait got the desktop row with six controls
and the search box squeezed down to its icon. The compact layout already
carries every filter in its drawer, so the handover moves to lg (1024px)
and tablets get a usable search box.

Lists also sized themselves off the viewport with hard-coded pixel
offsets - max-h-[calc(100dvh-231px)] and -276px. Those went stale the
moment a toolbar changed height, which is what put a second scrollbar on
the page next to the table's own. Both now flex into the space the
toolbar leaves, so the visible row count follows the window instead of a
fixed guess: the repository table shows 13 rows at 800px and 23 at
1400px, where it used to stop at the same count either way.

Also drops the bottom status bar from both lists. Its count moves up into
the empty left half of the filter row, and its live indicator was saying
what the header's LIVE button already says.

Claude-Session: https://claude.ai/code/session_01QRhZnpehnrVtnPRqJYDdVQ
This commit is contained in:
Arunavo Ray committed 2026-09-01 10:07:35 +05:30
1 parent cadeb64dce
commit 4ac2c4323c
5 files changed
+78 -100

No files matched your search

@@ -1,6 +1,5 @@
import * as React from "react";
import { ChevronsUpDown, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ChevronDown, Check } from "lucide-react";
import {
Command,
CommandEmpty,
@@ -14,15 +13,25 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { selectTriggerClassName } from "@/components/ui/select";
import { cn } from "@/lib/utils";
type ActivityNameComboboxProps = {
activities: any[];
value: string;
onChange: (value: string) => void;
className?: string;
};
export function ActivityNameCombobox({ activities, value, onChange }: ActivityNameComboboxProps) {
/** Stays a combobox rather than a Select because the name list grows with the
* activity log and needs the search box. The trigger borrows the Select
* styling so it reads as the same control as the status and type dropdowns. */
export function ActivityNameCombobox({
activities,
value,
onChange,
className,
}: ActivityNameComboboxProps) {
// Collect unique names from repositoryName and organizationName
const names = React.useMemo(() => {
const set = new Set<string>();
@@ -34,25 +43,30 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa
}, [activities]);
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
{/* The button stays inline: PopoverTrigger asChild clones its child to
attach the click handler and ref, which a wrapper component would
swallow unless it forwarded both. */}
<PopoverTrigger asChild>
<Button
variant="outline"
<button
type="button"
role="combobox"
aria-expanded={open}
className="w-full sm:w-[180px] justify-between h-10"
className={cn(
selectTriggerClassName,
"h-10 w-full lg:w-50",
className
)}
>
<span className={cn(
"truncate",
!value && "text-muted-foreground"
)}>
<span className={cn("truncate", !value && "text-muted-foreground")}>
{value || "All names"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
<ChevronDown className="size-4 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-0">
<PopoverContent className="w-50 p-0">
<Command>
<CommandInput placeholder="Search name..." />
<CommandList>
@@ -66,7 +80,12 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa
setOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", value === "" ? "opacity-100" : "opacity-0")} />
<Check
className={cn(
"mr-2 h-4 w-4",
value === "" ? "opacity-100" : "opacity-0"
)}
/>
All names
</CommandItem>
{names.map((name) => (
@@ -78,7 +97,12 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa
setOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", value === name ? "opacity-100" : "opacity-0")} />
<Check
className={cn(
"mr-2 h-4 w-4",
value === name ? "opacity-100" : "opacity-0"
)}
/>
{name}
</CommandItem>
))}
@@ -490,9 +490,9 @@ export function Organization() {
return (
<div className="flex flex-col gap-y-4 sm:gap-y-8">
{/* Search and filters */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full">
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-2 lg:gap-4 w-full">
{/* Mobile: Search bar with filter button */}
<div className="flex items-center gap-2 w-full sm:hidden">
<div className="flex items-center gap-2 w-full lg:hidden">
<div className="relative flex-grow">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
@@ -725,7 +725,7 @@ export function Organization() {
</div>
{/* Desktop: Original layout */}
<div className="hidden sm:flex sm:flex-row sm:items-center sm:gap-4 sm:w-full">
<div className="hidden lg:flex lg:flex-row lg:items-center lg:gap-4 lg:w-full">
<div className="relative flex-grow">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
+5 -6
View File
@@ -1034,11 +1034,11 @@ export default function Repository() {
};
return (
<div className="flex flex-col gap-y-4 sm:gap-y-8">
<div className="flex h-full min-h-0 flex-col gap-y-4 sm:gap-y-8">
{/* Search and filters */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full">
{/* Mobile: Search bar with filter button */}
<div className="flex items-center gap-2 w-full sm:hidden">
<div className="flex items-center gap-2 w-full lg:hidden">
<div className="relative flex-grow">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
@@ -1270,7 +1270,7 @@ export default function Repository() {
</div>
{/* Desktop: Original layout */}
<div className="hidden sm:flex sm:flex-row sm:items-center sm:gap-4 sm:w-full">
<div className="hidden lg:flex lg:flex-row lg:items-center lg:gap-4 lg:w-full">
<div className="relative flex-grow min-w-[180px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
@@ -1329,7 +1329,7 @@ export default function Repository() {
{/* Desktop: Bulk actions row - shown when repos are selected */}
{selectedRepoIds.size > 0 && (
<div className="hidden sm:flex items-center gap-2 flex-wrap">
<div className="hidden lg:flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
<span className="text-sm font-medium">
{selectedRepoIds.size} selected
@@ -1430,7 +1430,7 @@ export default function Repository() {
{/* Action buttons for mobile - only show when items are selected */}
{selectedRepoIds.size > 0 && (
<div className="flex items-center gap-2 flex-wrap sm:hidden">
<div className="flex items-center gap-2 flex-wrap lg:hidden">
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
<span className="text-sm font-medium">
{selectedRepoIds.size} selected
@@ -1552,7 +1552,6 @@ export default function Repository() {
<RepositoryTable
repositories={repositories}
isLoading={isInitialLoading || !connected}
isLiveActive={isLiveEnabled && isFullyConfigured}
filter={filter}
setFilter={setFilter}
onMirror={handleMirrorRepo}
@@ -52,7 +52,7 @@ function FilterCombobox({
type="button"
role="combobox"
aria-expanded={open}
className={cn(selectTriggerClassName, "h-10 w-full sm:w-50")}
className={cn(selectTriggerClassName, "h-10 w-full lg:w-50")}
>
<span className={cn("truncate", !value && "text-muted-foreground")}>
{value || emptyLabel}
+30 -75
View File
@@ -65,7 +65,6 @@ export const REPOSITORY_SORT_OPTIONS = [
interface RepositoryTableProps {
repositories: Repository[];
isLoading: boolean;
isLiveActive?: boolean;
filter: FilterParams;
setFilter: (filter: FilterParams) => void;
onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
@@ -108,7 +107,6 @@ function getTableSorting(sortOrder: string | undefined): SortingState {
export default function RepositoryTable({
repositories,
isLoading,
isLiveActive = false,
filter,
setFilter,
onMirror,
@@ -684,38 +682,35 @@ export default function RepositoryTable({
/* Sits above the table in both the loading and loaded states so the
controls do not appear and disappear as data arrives. */
const filterRow = (
<div
className={cn(
"flex flex-wrap items-center justify-between gap-3 sm:mb-4",
hasAnyFilter && "mb-4"
)}
>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{hasAnyFilter
? `Showing ${visibleRepositories.length} of ${repositories.length} repositories`
: `${repositories.length} ${
repositories.length === 1 ? "repository" : "repositories"
}`}
</span>
{hasAnyFilter && (
<>
<span className="text-sm text-muted-foreground">
Showing {visibleRepositories.length} of {repositories.length} repositories
</span>
<Button
variant="ghost"
size="sm"
onClick={() =>
setFilter({
searchTerm: "",
status: "",
organization: "",
owner: "",
sort: filter.sort || "imported-desc",
})
}
>
Clear filters
</Button>
</>
<Button
variant="ghost"
size="sm"
onClick={() =>
setFilter({
searchTerm: "",
status: "",
organization: "",
owner: "",
sort: filter.sort || "imported-desc",
})
}
>
Clear filters
</Button>
)}
</div>
<div className="hidden flex-wrap items-center gap-2 sm:flex">
<div className="hidden flex-wrap items-center gap-2 lg:flex">
<Select
value={filter.status || "all"}
onValueChange={(value) =>
@@ -871,7 +866,7 @@ export default function RepositoryTable({
</div>
</div>
) : (
<div>
<div className="flex h-full min-h-0 flex-col">
{filterRow}
{visibleRepositories.length === 0 ? (
@@ -883,8 +878,8 @@ export default function RepositoryTable({
</p>
</div>
) : (
<>
{/* Mobile card view */}
<div className="flex min-h-0 flex-1 flex-col">
{/* Mobile card view: flows in the page scroll rather than its own. */}
<div className="lg:hidden pb-20">
{/* Select all checkbox */}
<div className="flex items-center gap-3 mb-3 p-3 bg-muted/50 rounded-md">
@@ -906,7 +901,7 @@ export default function RepositoryTable({
</div>
{/* Desktop table view */}
<div className="hidden lg:flex flex-col border rounded-md">
<div className="hidden lg:flex min-h-0 flex-1 flex-col border rounded-md">
{/* Table header */}
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
@@ -938,7 +933,7 @@ export default function RepositoryTable({
{/* Table body wrapper (for a parent in virtualization) */}
<div
ref={tableParentRef}
className="flex flex-col max-h-[calc(100dvh-276px)] overflow-y-auto"
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
>
<div
style={{
@@ -1147,48 +1142,8 @@ export default function RepositoryTable({
})}
</div>
</div>
{/* Status Bar */}
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3 relative">
<div className="flex items-center gap-2">
<div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
<span className="text-sm font-medium text-foreground">
{hasAnyFilter
? `Showing ${visibleRepositories.length} of ${repositories.length} repositories`
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
</span>
</div>
{/* Center - Live active indicator */}
{isLiveActive && (
<div className="flex items-center gap-1.5 absolute left-1/2 transform -translate-x-1/2">
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite'
}}
/>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
Live active
</span>
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite',
animationDelay: '1s'
}}
/>
</div>
)}
{hasAnyFilter && (
<span className="text-xs text-muted-foreground">
Filters applied
</span>
)}
</div>
</div>
</>
</div>
)}
<MirrorOverridesDialog