From b2d2f28b40b56315c1ef407cc6343b91f1e1b26f Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:57:39 +0100 Subject: [PATCH] style: redesign dashboard (#586) style: redesign dashboard style(tables): same height row in all pages --- app/client/components/app-sidebar.tsx | 39 ++++++-- app/client/components/grid-background.tsx | 21 ++-- app/client/components/layout.tsx | 62 +++++++----- app/client/components/onoff.tsx | 2 +- app/client/components/status-dot.tsx | 4 +- app/client/components/ui/button.tsx | 5 +- app/client/components/ui/card.tsx | 17 +++- app/client/components/ui/input.tsx | 1 + app/client/components/ui/select.tsx | 2 +- app/client/components/ui/tabs.tsx | 2 +- app/client/components/volume-icon.tsx | 2 +- .../backups/components/backup-card.tsx | 33 ++++--- app/client/modules/backups/routes/backups.tsx | 19 ++-- .../routes/notification-details.tsx | 2 +- .../notifications/routes/notifications.tsx | 64 ++++++------ .../repositories/routes/repositories.tsx | 97 +++++++++---------- .../settings/components/user-management.tsx | 2 +- .../volumes/components/create-volume-form.tsx | 4 +- app/client/modules/volumes/routes/volumes.tsx | 24 ++--- app/routes/(dashboard)/route.tsx | 2 +- 20 files changed, 233 insertions(+), 171 deletions(-) diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 317e5cea..6c5f301f 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -53,6 +53,8 @@ export function AppSidebar() { const { updates, hasUpdate } = useUpdates(); const [showReleaseNotes, setShowReleaseNotes] = useState(false); + const isCollapsed = state === "collapsed"; + const displayVersion = APP_VERSION.startsWith("v") || APP_VERSION === "dev" ? APP_VERSION : `v${APP_VERSION}`; const releaseUrl = APP_VERSION === "dev" @@ -66,7 +68,7 @@ export function AppSidebar() { Zerobyte @@ -82,18 +84,43 @@ export function AppSidebar() { - - isMobile && setOpenMobile(false)}> + + isMobile && setOpenMobile(false)} + activeProps={{ className: "bg-strong-accent/10" }} + className="w-full flex items-center gap-2" + > {({ isActive }) => ( <> - - {item.title} + {isActive && ( + + )} + + + {item.title} + > )} - + {item.title} diff --git a/app/client/components/grid-background.tsx b/app/client/components/grid-background.tsx index 649ddffd..5c46461e 100644 --- a/app/client/components/grid-background.tsx +++ b/app/client/components/grid-background.tsx @@ -9,16 +9,17 @@ interface GridBackgroundProps { export function GridBackground({ children, className, containerClassName }: GridBackgroundProps) { return ( - - {children} + + + {children} ); } diff --git a/app/client/components/layout.tsx b/app/client/components/layout.tsx index 8593e31d..804c0c36 100644 --- a/app/client/components/layout.tsx +++ b/app/client/components/layout.tsx @@ -1,4 +1,4 @@ -import { LifeBuoy } from "lucide-react"; +import { LifeBuoy, LogOut } from "lucide-react"; import { toast } from "sonner"; import { type AppContext } from "~/context"; import { GridBackground } from "./grid-background"; @@ -9,6 +9,7 @@ import { authClient } from "../lib/auth-client"; import { DevPanelListener } from "./dev-panel-listener"; import { Outlet, useNavigate } from "@tanstack/react-router"; import { AppBreadcrumb } from "./app-breadcrumb"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; type Props = { loaderData: AppContext; @@ -34,34 +35,49 @@ export function Layout({ loaderData }: Props) { - - + + {loaderData.user && ( - - - Welcome, - {loaderData.user?.username} + + + {loaderData.user?.username} - - Logout - - - - - - Report an issue - - - + + + + + + + Logout + + + + + + + + + + Report an issue + )} diff --git a/app/client/components/onoff.tsx b/app/client/components/onoff.tsx index 8161bc32..7fe95082 100644 --- a/app/client/components/onoff.tsx +++ b/app/client/components/onoff.tsx @@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P className={cn( "flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors", isOn - ? "border-green-200 bg-green-50 text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-200" + ? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200" : "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10", )} > diff --git a/app/client/components/status-dot.tsx b/app/client/components/status-dot.tsx index 19a42c44..a1a67cdf 100644 --- a/app/client/components/status-dot.tsx +++ b/app/client/components/status-dot.tsx @@ -12,7 +12,7 @@ interface StatusDotProps { export const StatusDot = ({ variant, label, animated }: StatusDotProps) => { const statusMapping = { success: { - color: "bg-green-500", + color: "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]", colorLight: "bg-emerald-400", animated: animated ?? true, }, @@ -45,7 +45,7 @@ export const StatusDot = ({ variant, label, animated }: StatusDotProps) => { {statusMapping?.animated && ( diff --git a/app/client/components/ui/button.tsx b/app/client/components/ui/button.tsx index 48f37a75..9f2411b3 100644 --- a/app/client/components/ui/button.tsx +++ b/app/client/components/ui/button.tsx @@ -7,11 +7,12 @@ import { cn } from "~/client/lib/utils"; import { useMinimumDuration } from "~/client/hooks/useMinimumDuration"; const buttonVariants = cva( - "inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0", + "inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0", { variants: { variant: { - default: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80", + default: + "bg-transparent text-white hover:bg-[#3A3A3A]/80 border hover:border-white/40 dark:text-white dark:hover:bg-[#3A3A3A]/80", primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50", destructive: "border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white", diff --git a/app/client/components/ui/card.tsx b/app/client/components/ui/card.tsx index 20f21ad1..1ff9e505 100644 --- a/app/client/components/ui/card.tsx +++ b/app/client/components/ui/card.tsx @@ -2,14 +2,25 @@ import type * as React from "react"; import { cn } from "~/client/lib/utils"; -function Card({ className, children, ...props }: React.ComponentProps<"div">) { +function Card({ className, children, interactive, ...props }: React.ComponentProps<"div"> & { interactive?: boolean }) { return ( - + diff --git a/app/client/components/ui/input.tsx b/app/client/components/ui/input.tsx index d8022b56..16fda709 100644 --- a/app/client/components/ui/input.tsx +++ b/app/client/components/ui/input.tsx @@ -9,6 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { data-slot="input" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + // "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-black/5 dark:bg-white/5 flex h-9 w-full min-w-0 border-0 px-3 py-1 text-sm shadow-xs dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className, diff --git a/app/client/components/ui/select.tsx b/app/client/components/ui/select.tsx index 210bc6ac..eaf16419 100644 --- a/app/client/components/ui/select.tsx +++ b/app/client/components/ui/select.tsx @@ -29,7 +29,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} diff --git a/app/client/components/ui/tabs.tsx b/app/client/components/ui/tabs.tsx index 27fff91a..efef2882 100644 --- a/app/client/components/ui/tabs.tsx +++ b/app/client/components/ui/tabs.tsx @@ -33,7 +33,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps { const { icon: Icon, label } = getIconAndLabel(backend); return ( - + {label} diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 935cb938..2bf7b337 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -8,7 +8,7 @@ import { Link } from "@tanstack/react-router"; export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { return ( - + @@ -23,25 +23,32 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { - {schedule.volume.name} + {schedule.volume.name} → - {schedule.repository.name} + {schedule.repository.name} - - - Schedule - {schedule.cronExpression} + + + Schedule + + + {schedule.cronExpression} + - - Last backup - {formatTimeAgo(schedule.lastBackupAt)} + + Last backup + + {formatTimeAgo(schedule.lastBackupAt)} - - Next backup - {formatShortDateTime(schedule.nextBackupAt)} + + Next backup + + + {formatShortDateTime(schedule.nextBackupAt)} + diff --git a/app/client/modules/backups/routes/backups.tsx b/app/client/modules/backups/routes/backups.tsx index 9de3128c..b9e5525c 100644 --- a/app/client/modules/backups/routes/backups.tsx +++ b/app/client/modules/backups/routes/backups.tsx @@ -13,7 +13,6 @@ import { CalendarClock, Plus } from "lucide-react"; import { useState } from "react"; import { EmptyState } from "~/client/components/empty-state"; import { Button } from "~/client/components/ui/button"; -import { Card, CardContent } from "~/client/components/ui/card"; import { listBackupSchedulesOptions, reorderBackupSchedulesMutation, @@ -110,13 +109,17 @@ export function BackupsPage() { ); })} - - - - - Create a backup job - - + + + + + + + + Create a backup job + + + diff --git a/app/client/modules/notifications/routes/notification-details.tsx b/app/client/modules/notifications/routes/notification-details.tsx index 150694f7..2cba46de 100644 --- a/app/client/modules/notifications/routes/notification-details.tsx +++ b/app/client/modules/notifications/routes/notification-details.tsx @@ -97,7 +97,7 @@ export function NotificationDetailsPage({ notificationId }: { notificationId: st diff --git a/app/client/modules/notifications/routes/notifications.tsx b/app/client/modules/notifications/routes/notifications.tsx index 03f9c19c..836a7d41 100644 --- a/app/client/modules/notifications/routes/notifications.tsx +++ b/app/client/modules/notifications/routes/notifications.tsx @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; import { listNotificationDestinationsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { useNavigate } from "@tanstack/react-router"; +import { cn } from "~/client/lib/utils"; export function NotificationsPage() { const [searchQuery, setSearchQuery] = useState(""); @@ -57,15 +58,15 @@ export function NotificationsPage() { return ( - + setSearchQuery(e.target.value)} /> - + @@ -80,7 +81,7 @@ export function NotificationsPage() { - + @@ -110,36 +111,33 @@ export function NotificationsPage() { - {hasNoFilteredNotifications ? ( - - - - No destinations match your filters. - - - Clear filters - - + + + + No destinations match your filters. + + + Clear filters + + + + + {filteredNotifications.map((notification) => ( + navigate({ to: `/notifications/${notification.id}` })} + > + {notification.name} + {notification.type} + + - ) : ( - filteredNotifications.map((notification) => ( - navigate({ to: `/notifications/${notification.id}` })} - > - {notification.name} - {notification.type} - - - - - )) - )} + ))} diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx index 22927c4d..51a9f40d 100644 --- a/app/client/modules/repositories/routes/repositories.tsx +++ b/app/client/modules/repositories/routes/repositories.tsx @@ -58,15 +58,15 @@ export function RepositoriesPage() { return ( - + setSearchQuery(e.target.value)} /> - + @@ -76,7 +76,7 @@ export function RepositoriesPage() { - + @@ -109,53 +109,50 @@ export function RepositoriesPage() { - {hasNoFilteredRepositories ? ( - - - - No repositories match your filters. - - - Clear filters - - + + + + No repositories match your filters. + + + Clear filters + + + + + {filteredRepositories.map((repository) => ( + navigate({ to: `/repositories/${repository.shortId}` })} + > + {repository.name} + + + + {repository.type} + + + + + {repository.compressionMode || "off"} + + + + + {repository.status || "unknown"} + - ) : ( - filteredRepositories.map((repository) => ( - navigate({ to: `/repositories/${repository.shortId}` })} - > - {repository.name} - - - - {repository.type} - - - - - {repository.compressionMode || "off"} - - - - - {repository.status || "unknown"} - - - - )) - )} + ))} diff --git a/app/client/modules/settings/components/user-management.tsx b/app/client/modules/settings/components/user-management.tsx index d64ac359..ccb96a3f 100644 --- a/app/client/modules/settings/components/user-management.tsx +++ b/app/client/modules/settings/components/user-management.tsx @@ -144,7 +144,7 @@ export function UserManagement() { Banned - + Active diff --git a/app/client/modules/volumes/components/create-volume-form.tsx b/app/client/modules/volumes/components/create-volume-form.tsx index 1497b5ea..8963314e 100644 --- a/app/client/modules/volumes/components/create-volume-form.tsx +++ b/app/client/modules/volumes/components/create-volume-form.tsx @@ -237,7 +237,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for > {testBackendConnection.isPending && } {!testBackendConnection.isPending && testMessage?.success && ( - + )} {!testBackendConnection.isPending && testMessage && !testMessage.success && ( @@ -255,7 +255,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for {testMessage && ( diff --git a/app/client/modules/volumes/routes/volumes.tsx b/app/client/modules/volumes/routes/volumes.tsx index e6a6aeef..ba2f10af 100644 --- a/app/client/modules/volumes/routes/volumes.tsx +++ b/app/client/modules/volumes/routes/volumes.tsx @@ -70,15 +70,15 @@ export function VolumesPage() { return ( - + setSearchQuery(e.target.value)} /> - + @@ -88,7 +88,7 @@ export function VolumesPage() { - + @@ -135,14 +135,14 @@ export function VolumesPage() { filteredVolumes.map((volume) => ( navigate({ to: `/volumes/${volume.shortId}` })} > - {volume.name} - + {volume.name} + - + - + {hasNoFilteredVolumes ? ( "No volumes match filters." ) : ( - - {filteredVolumes.length} volume + + {filteredVolumes.length} volume {filteredVolumes.length > 1 ? "s" : ""} )} diff --git a/app/routes/(dashboard)/route.tsx b/app/routes/(dashboard)/route.tsx index 5698b32b..c9116306 100644 --- a/app/routes/(dashboard)/route.tsx +++ b/app/routes/(dashboard)/route.tsx @@ -13,7 +13,7 @@ export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { const hasUsers = await authService.hasUsers(); const sidebarCookie = getCookie(SIDEBAR_COOKIE_NAME); - const sidebarOpen = sidebarCookie === null ? true : sidebarCookie === "true"; + const sidebarOpen = !sidebarCookie ? true : sidebarCookie === "true"; return { user: session?.user ?? null, hasUsers, sidebarOpen }; });
{item.title}
{schedule.cronExpression}
+ {schedule.cronExpression} +
No destinations match your filters.
No repositories match your filters.