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

View File

@@ -6,15 +6,7 @@ import { createVolumeMutation } from "~/api-client/@tanstack/react-query.gen";
import { parseError } from "~/lib/errors";
import { CreateVolumeForm } from "./create-volume-form";
import { Button } from "./ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
import { ScrollArea } from "./ui/scroll-area";
type Props = {
@@ -41,7 +33,7 @@ export const CreateVolumeDialog = ({ open, setOpen }: Props) => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<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" />
Create volume
</Button>

View File

@@ -1,13 +1,13 @@
import { useMutation } from "@tanstack/react-query";
import { Outlet, useNavigate } from "react-router";
import { toast } from "sonner";
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
import { appContext } from "~/context";
import { cn } from "~/lib/utils";
import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/layout";
import { AppBreadcrumb } from "./app-breadcrumb";
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];
@@ -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)]",
)}
>
<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>
<main className="relative flex flex-col pt-4 sm:pt-8 px-2 sm:px-4 pb-4 container mx-auto">
<div className="flex items-center justify-between mb-4">
<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>
<header className="relative bg-card-header border-b border-border/50">
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-4 container mx-auto">
<AppBreadcrumb />
{loaderData.user && (
<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}>
Logout
</Button>
</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 />
</main>
</div>

View File

@@ -6,23 +6,23 @@ import type * as React from "react";
import { cn } from "~/lib/utils";
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: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"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",
outline:
"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: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
default:
"bg-[var(--strong-accent)] text-white hover:bg-[var(--strong-accent)]/90 focus-visible:ring-[var(--strong-accent)]/50",
destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50",
outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"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",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "rounded-xs h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
default: "h-9 px-5 py-2 has-[>svg]:px-4",
sm: "h-8 px-3 py-1.5 has-[>svg]:px-2.5",
lg: "h-10 px-6 py-2.5 has-[>svg]:px-5",
icon: "size-9",
},
},

View File

@@ -42,10 +42,10 @@ const getIconAndColor = (backend: BackendType) => {
};
export const VolumeIcon = ({ backend, size = 10 }: VolumeIconProps) => {
const { icon: Icon, color, label } = getIconAndColor(backend);
const { icon: Icon, label } = getIconAndColor(backend);
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} />
{label}
</span>

View File

@@ -18,7 +18,7 @@ export function StorageChart({ statfs }: Props) {
{
name: "Used",
value: statfs.used,
fill: "#2B7EFF",
fill: "#ff543a",
},
{
name: "Free",
@@ -63,7 +63,7 @@ export function StorageChart({ statfs }: Props) {
</CardTitle>
</CardHeader>
<CardContent className="flex-1 pb-0">
<div className="">
<div>
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
<PieChart>
<ChartTooltip
@@ -105,9 +105,9 @@ export function StorageChart({ statfs }: Props) {
<ByteSize bytes={statfs.total} className="font-mono text-sm" />
</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="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>
</div>
<div className="text-right">

View File

@@ -9,9 +9,10 @@ import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
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 type { Route } from "./+types/home";
import { Card, CardHeader } from "~/components/ui/card";
export function meta(_: Route.MetaArgs) {
return [
@@ -60,85 +61,96 @@ export default function Home({ loaderData }: Route.ComponentProps) {
return (
<>
<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">
Create, manage, monitor, and automate your volumes with ease.
</h2>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 mt-4 sm:justify-between">
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<Input
className="w-full sm:w-[180px]"
placeholder="Search volumes…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="All status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mounted">Mounted</SelectItem>
<SelectItem value="unmounted">Unmounted</SelectItem>
<SelectItem value="error">Error</SelectItem>
</SelectContent>
</Select>
<Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="All backends" />
</SelectTrigger>
<SelectContent>
<SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
</SelectContent>
</Select>
{(searchQuery || statusFilter || backendFilter) && (
<Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
)}
</span>
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
</div>
<div className="mt-4 overflow-x-auto">
<Table className="border bg-white dark:bg-secondary">
<TableCaption>A list of your managed volumes.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] uppercase">Name</TableHead>
<TableHead className="uppercase text-left">Backend</TableHead>
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</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>
{/* <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"> */}
{/* Create, manage, monitor, and automate your volumes with ease. */}
{/* </h2> */}
<Card className="p-0 gap-0">
<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">
<span className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<Input
className="w-full sm:w-[180px]"
placeholder="Search volumes…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="All status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mounted">Mounted</SelectItem>
<SelectItem value="unmounted">Unmounted</SelectItem>
<SelectItem value="error">Error</SelectItem>
</SelectContent>
</Select>
<Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="All backends" />
</SelectTrigger>
<SelectContent>
<SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
</SelectContent>
</Select>
{(searchQuery || statusFilter || backendFilter) && (
<Button variant="outline" size="sm" onClick={clearFilters} className="w-full sm:w-auto">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
)}
</span>
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
</div>
<div className="overflow-x-auto">
<Table className="border-t">
<TableHeader className="bg-card-header">
<TableRow>
<TableHead className="w-[100px] uppercase">Name</TableHead>
<TableHead className="uppercase text-left">Backend</TableHead>
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead>
<TableHead className="uppercase text-center">Status</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</div>
</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 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 (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<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)]">
<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>
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription>Sign in to your account</CardDescription>

View File

@@ -60,8 +60,9 @@ export default function OnboardingPage() {
};
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<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)]">
<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>
<CardTitle className="text-2xl font-bold">Welcome to Ironmount</CardTitle>
<CardDescription>Create the admin user to get started</CardDescription>