ui: redesign

This commit is contained in:
Nicolas Meienberger
2025-10-03 23:54:53 +02:00
parent 689f14dff7
commit e134d0e1d1
9 changed files with 137 additions and 121 deletions

View File

@@ -11,7 +11,7 @@
html, html,
body { body {
@apply bg-white dark:bg-[#0D0D0D]; @apply bg-white dark:bg-[#131313];
overflow-x: hidden; overflow-x: hidden;
width: 100%; width: 100%;
position: relative; position: relative;
@@ -30,6 +30,7 @@ body {
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-card-header: var(--card-header);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
@@ -57,6 +58,7 @@ body {
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-strong-accent: var(--strong-accent);
} }
:root { :root {
@@ -65,6 +67,7 @@ body {
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--card-header: oklch(0.922 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0); --primary: oklch(0.205 0 0);
@@ -92,12 +95,14 @@ body {
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--strong-accent: #ff543a;
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: #131313;
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.1448 0 0); --card: #131313;
--card-header: #1b1b1b;
/* --card: oklch(0.205 0 0); ORIGINAL */ /* --card: oklch(0.205 0 0); ORIGINAL */
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0); --popover: oklch(0.205 0 0);
@@ -128,6 +133,7 @@ body {
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
--strong-accent: #ff543a;
} }
@layer base { @layer base {

View File

@@ -6,15 +6,7 @@ import { createVolumeMutation } from "~/api-client/@tanstack/react-query.gen";
import { parseError } from "~/lib/errors"; import { parseError } from "~/lib/errors";
import { CreateVolumeForm } from "./create-volume-form"; import { CreateVolumeForm } from "./create-volume-form";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { ScrollArea } from "./ui/scroll-area"; import { ScrollArea } from "./ui/scroll-area";
type Props = { type Props = {
@@ -41,7 +33,7 @@ export const CreateVolumeDialog = ({ open, setOpen }: Props) => {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-blue-900 hover:bg-blue-800"> <Button className="bg-transparent border hover:bg-strong-accent">
<Plus size={16} className="mr-2" /> <Plus size={16} className="mr-2" />
Create volume Create volume
</Button> </Button>

View File

@@ -1,13 +1,13 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { Outlet, useNavigate } from "react-router"; import { Outlet, useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
import { appContext } from "~/context";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/layout";
import { AppBreadcrumb } from "./app-breadcrumb"; import { AppBreadcrumb } from "./app-breadcrumb";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
import type { Route } from "./+types/layout";
import { appContext } from "~/context";
import { authMiddleware } from "~/middleware/auth";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];
@@ -39,19 +39,23 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", "dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
)} )}
> >
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div> <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-card"></div>
<main className="relative flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto"> <header className="relative bg-card-header border-b border-border/50">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
<AppBreadcrumb /> <AppBreadcrumb />
{loaderData.user && ( {loaderData.user && (
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground">Welcome, {loaderData.user?.username}</span> <span className="text-sm text-muted-foreground">
Welcome, <span className="text-strong-accent">{loaderData.user?.username}</span>
</span>
<Button variant="outline" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}> <Button variant="outline" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}>
Logout Logout
</Button> </Button>
</div> </div>
)} )}
</div> </div>
</header>
<main className="relative flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto">
<Outlet /> <Outlet />
</main> </main>
</div> </div>

View File

@@ -6,23 +6,23 @@ import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex cursor-pointer uppercase border items-center justify-center dark:border-white dark:bg-secondary dark:text-white gap-2 whitespace-nowrap text-sm font-medium 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: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", "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",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", default:
destructive: "bg-[var(--strong-accent)] text-white hover:bg-[var(--strong-accent)]/90 focus-visible:ring-[var(--strong-accent)]/50",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50",
outline: outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary:
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-[#464646] text-white hover:bg-[#464646]/80 dark:bg-[#464646] dark:text-white dark:hover:bg-[#464646]/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-5 py-2 has-[>svg]:px-4",
sm: "rounded-xs h-8 gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 px-3 py-1.5 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4", lg: "h-10 px-6 py-2.5 has-[>svg]:px-5",
icon: "size-9", icon: "size-9",
}, },
}, },

View File

@@ -42,10 +42,10 @@ const getIconAndColor = (backend: BackendType) => {
}; };
export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => { export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => {
const { icon: Icon, color, label } = getIconAndColor(backend); const { icon: Icon, label } = getIconAndColor(backend);
return ( return (
<span className={`flex items-center gap-2 ${color} rounded-md px-2 py-1`}> <span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
<Icon size={size} /> <Icon size={size} />
{label} {label}
</span> </span>

View File

@@ -18,7 +18,7 @@ export function StorageChart({ statfs }: Props) {
{ {
name: "Used", name: "Used",
value: statfs.used, value: statfs.used,
fill: "#2B7EFF", fill: "#ff543a",
}, },
{ {
name: "Free", name: "Free",
@@ -63,7 +63,7 @@ export function StorageChart({ statfs }: Props) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex-1 pb-0"> <CardContent className="flex-1 pb-0">
<div className=""> <div>
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]"> <ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
<PieChart> <PieChart>
<ChartTooltip <ChartTooltip
@@ -105,9 +105,9 @@ export function StorageChart({ statfs }: Props) {
<ByteSize bytes={statfs.total} className="font-mono text-sm" /> <ByteSize bytes={statfs.total} className="font-mono text-sm" />
</div> </div>
<div className="flex items-center justify-between p-3 rounded-lg bg-blue-500/10"> <div className="flex items-center justify-between p-3 rounded-lg bg-strong-accent/10">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-4 w-4 rounded-full bg-blue-500" /> <div className="h-4 w-4 rounded-full bg-strong-accent" />
<span className="font-medium">Used Space</span> <span className="font-medium">Used Space</span>
</div> </div>
<div className="text-right"> <div className="text-right">

View File

@@ -9,9 +9,10 @@ import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { VolumeIcon } from "~/components/volume-icon"; import { VolumeIcon } from "~/components/volume-icon";
import type { Route } from "./+types/home"; import type { Route } from "./+types/home";
import { Card, CardHeader } from "~/components/ui/card";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
@@ -60,85 +61,96 @@ export default function Home({ loaderData }: Route.ComponentProps) {
return ( return (
<> <>
<h1 className="text-2xl sm:text-3xl font-bold mb-0 uppercase">Ironmount</h1> {/* <h1 className="text-2xl sm:text-3xl font-bold mb-0 uppercase">Ironmount</h1> */}
<h2 className="text-xs sm:text-sm font-semibold mb-2 text-muted-foreground"> {/* <h2 className="text-xs sm:text-sm font-semibold mb-2 text-muted-foreground"> */}
Create, manage, monitor, and automate your volumes with ease. {/* Create, manage, monitor, and automate your volumes with ease. */}
</h2> {/* </h2> */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mt-4 sm:justify-between"> <Card className="p-0 gap-0">
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:justify-between p-4 bg-card-header py-4">
<Input <span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
className="w-full sm:w-[180px]" <Input
placeholder="Search volumes…" className="w-full sm:w-[180px]"
value={searchQuery} placeholder="Search volumes…"
onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery}
/> onChange={(e) => setSearchQuery(e.target.value)}
<Select value={statusFilter} onValueChange={setStatusFilter}> />
<SelectTrigger className="w-full sm:w-[180px]"> <Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectValue placeholder="All status" /> <SelectTrigger className="w-full sm:w-[180px]">
</SelectTrigger> <SelectValue placeholder="All status" />
<SelectContent> </SelectTrigger>
<SelectItem value="mounted">Mounted</SelectItem> <SelectContent>
<SelectItem value="unmounted">Unmounted</SelectItem> <SelectItem value="mounted">Mounted</SelectItem>
<SelectItem value="error">Error</SelectItem> <SelectItem value="unmounted">Unmounted</SelectItem>
</SelectContent> <SelectItem value="error">Error</SelectItem>
</Select> </SelectContent>
<Select value={backendFilter} onValueChange={setBackendFilter}> </Select>
<SelectTrigger className="w-full sm:w-[180px]"> <Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectValue placeholder="All backends" /> <SelectTrigger className="w-full sm:w-[180px]">
</SelectTrigger> <SelectValue placeholder="All backends" />
<SelectContent> </SelectTrigger>
<SelectItem value="directory">Directory</SelectItem> <SelectContent>
<SelectItem value="nfs">NFS</SelectItem> <SelectItem value="directory">Directory</SelectItem>
<SelectItem value="smb">SMB</SelectItem> <SelectItem value="nfs">NFS</SelectItem>
</SelectContent> <SelectItem value="smb">SMB</SelectItem>
</Select> </SelectContent>
{(searchQuery || statusFilter || backendFilter) && ( </Select>
<Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto"> {(searchQuery || statusFilter || backendFilter) && (
<RotateCcw className="h-4 w-4 mr-2" /> <Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto">
Clear filters <RotateCcw className="h-4 w-4 mr-2" />
</Button> Clear filters
)} </Button>
</span> )}
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} /> </span>
</div> <CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
<div className="mt-4 overflow-x-auto"> </div>
<Table className="border bg-white dark:bg-secondary"> <div className="overflow-x-auto">
<TableCaption>A list of your managed volumes.</TableCaption> <Table className="border-t">
<TableHeader> <TableHeader className="bg-card-header">
<TableRow> <TableRow>
<TableHead className="w-[100px] uppercase">Name</TableHead> <TableHead className="w-[100px] uppercase">Name</TableHead>
<TableHead className="uppercase text-left">Backend</TableHead> <TableHead className="uppercase text-left">Backend</TableHead>
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead> <TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead>
<TableHead className="uppercase text-center">Status</TableHead> <TableHead className="uppercase text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-accent/50 hover:cursor-pointer"
onClick={() => navigate(`/volumes/${volume.name}`)}
>
<TableCell className="font-medium">{volume.name}</TableCell>
<TableCell>
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="hidden sm:table-cell">
<span className="flex items-center gap-2">
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.path}
</span>
<Copy size={10} />
</span>
</TableCell>
<TableCell className="text-center">
<StatusDot status={volume.status} />
</TableCell>
</TableRow> </TableRow>
))} </TableHeader>
</TableBody> <TableBody>
</Table> {filteredVolumes.map((volume) => (
</div> <TableRow
key={volume.name}
className="hover:bg-accent/50 hover:cursor-pointer"
onClick={() => navigate(`/volumes/${volume.name}`)}
>
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
<TableCell>
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="hidden sm:table-cell">
<span className="flex items-center gap-2">
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.path}
</span>
<Copy size={10} />
</span>
</TableCell>
<TableCell className="text-center">
<StatusDot status={volume.status} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
{filteredVolumes.length === 0 ? (
"No volumes found."
) : (
<span>
<span className="text-strong-accent">{filteredVolumes.length}</span> volume
{filteredVolumes.length > 1 ? "s" : ""}
</span>
)}
</div>
</Card>
</> </>
); );
} }

View File

@@ -49,8 +49,9 @@ export default function LoginPage() {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center p-4"> <div className="relative min-h-screen flex items-center justify-center p-4 [background-size:20px_20px] sm:[background-size:40px_40px] [background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)] dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]">
<Card className="w-full max-w-md"> <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black" />
<Card className="relative w-full max-w-md">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle> <CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription>Sign in to your account</CardDescription> <CardDescription>Sign in to your account</CardDescription>

View File

@@ -60,8 +60,9 @@ export default function OnboardingPage() {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center p-4"> <div className="relative min-h-screen flex items-center justify-center p-4 [background-size:20px_20px] sm:[background-size:40px_40px] [background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)] dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]">
<Card className="w-full max-w-md"> <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black" />
<Card className="relative w-full max-w-md">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle> <CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle>
<CardDescription>Create the admin user to get started</CardDescription> <CardDescription>Create the admin user to get started</CardDescription>