mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-10-30 22:21:58 +01:00
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:
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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user