🎨 style: establish console design system

This commit is contained in:
2026-08-13 02:19:26 +07:00
parent 5e88c660dc
commit 3a09f0b571
26 changed files with 840 additions and 269 deletions
+83 -63
View File
@@ -1,10 +1,12 @@
"use client"; "use client";
import { Loader2 } from "lucide-react"; import { ArrowRight, Check } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { AuthFormCard, FormError } from "@/components/auth/auth-form-card";
import { BrandMark } from "@/components/brand";
import { FullScreenLoader } from "@/components/page-layout";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/lib/api-client"; import { api } from "@/lib/api-client";
@@ -77,68 +79,86 @@ export default function BootstrapPage() {
} }
}; };
if (checkingStatus) { if (checkingStatus) return <FullScreenLoader label="Checking setup" />;
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4"> <main className="auth-grid flex min-h-screen items-center justify-center bg-sidebar p-5 sm:p-10">
<Card className="w-full max-w-md"> <div className="grid w-full max-w-5xl overflow-hidden border border-sidebar-border bg-background text-foreground shadow-[12px_12px_0_color-mix(in_oklch,var(--sidebar-primary)_18%,transparent)] lg:grid-cols-[0.8fr_1.2fr]">
<CardHeader className="space-y-1"> <section className="flex flex-col justify-between bg-primary p-8 text-primary-foreground sm:p-10">
<CardTitle className="text-2xl font-bold text-center">Welcome to Minikura</CardTitle> <BrandMark inverted />
<CardDescription className="text-center"> <div className="my-16">
Create your admin account to get started <span className="page-eyebrow text-primary-foreground/60">System bootstrap / 01</span>
</CardDescription> <h1 className="text-5xl font-black uppercase leading-[0.9] tracking-[-0.055em]">
</CardHeader> Build your command center.
<CardContent> </h1>
<form onSubmit={handleSubmit} className="space-y-4"> <p className="mt-5 text-sm leading-6 text-primary-foreground/65">
<div className="space-y-2"> The first account controls users, workloads, routing, and cluster visibility.
<Label htmlFor="name">Full Name</Label> </p>
<Input id="name" name="name" placeholder="John Doe" required autoFocus /> </div>
</div> <div className="space-y-3 font-mono text-[10px] font-bold uppercase tracking-[0.12em]">
<div className="space-y-2"> <p className="flex items-center gap-2">
<Label htmlFor="email">Email</Label> <Check className="size-3" /> Admin authority
<Input </p>
id="email" <p className="flex items-center gap-2">
name="email" <Check className="size-3" /> Secure session
type="email" </p>
placeholder="admin@example.com" <p className="flex items-center gap-2">
required <Check className="size-3" /> Ready in one step
/> </p>
</div> </div>
<div className="space-y-2"> </section>
<Label htmlFor="password">Password</Label> <AuthFormCard
<Input title="Create Operator"
id="password" description="Initialize Minikura with an administrator identity."
name="password" className="max-w-none rounded-none border-0 bg-card py-8 shadow-none sm:py-10"
type="password" headerClassName="px-7 sm:px-10"
placeholder="••••••••" contentClassName="px-7 sm:px-10"
minLength={8} >
required <form onSubmit={handleSubmit} className="space-y-5">
/> <div className="space-y-2">
</div> <Label htmlFor="name">Full Name</Label>
<div className="space-y-2"> <Input id="name" name="name" placeholder="John Doe" required autoFocus />
<Label htmlFor="confirmPassword">Confirm Password</Label> </div>
<Input <div className="space-y-2">
id="confirmPassword" <Label htmlFor="email">Email</Label>
name="confirmPassword" <Input
type="password" id="email"
placeholder="••••••••" name="email"
minLength={8} type="email"
required placeholder="admin@example.com"
/> required
</div> />
{error && <div className="text-sm text-red-600 text-center">{error}</div>} </div>
<Button type="submit" className="w-full" disabled={loading}> <div className="space-y-2">
{loading ? "Creating..." : "Create Admin Account"} <Label htmlFor="password">Password</Label>
</Button> <Input
</form> id="password"
</CardContent> name="password"
</Card> type="password"
</div> placeholder="Minimum 8 characters"
minLength={8}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
placeholder="Repeat password"
minLength={8}
required
/>
</div>
<FormError message={error} />
<Button type="submit" size="lg" className="w-full" disabled={loading}>
{loading ? "Initializing..." : "Initialize Console"}
{!loading && <ArrowRight className="ml-auto" />}
</Button>
</form>
</AuthFormCard>
</div>
</main>
); );
} }
+114 -33
View File
@@ -26,6 +26,10 @@
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
@@ -42,41 +46,49 @@
--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);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
} }
:root { :root {
--radius: 0.625rem; --radius: 0.25rem;
--background: oklch(1 0 0); --font-sans: "Arial Narrow", "Roboto Condensed", "Helvetica Neue", Arial, sans-serif;
--foreground: oklch(0.145 0 0); --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
--card: oklch(1 0 0); --background: oklch(0.965 0.012 95);
--card-foreground: oklch(0.145 0 0); --foreground: oklch(0.18 0.018 75);
--popover: oklch(1 0 0); --card: oklch(0.99 0.006 95);
--popover-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.18 0.018 75);
--primary: oklch(0.205 0 0); --popover: oklch(0.995 0.004 95);
--primary-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.18 0.018 75);
--secondary: oklch(0.97 0 0); --primary: oklch(0.75 0.205 125);
--secondary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.15 0.025 125);
--muted: oklch(0.97 0 0); --secondary: oklch(0.91 0.022 95);
--muted-foreground: oklch(0.556 0 0); --secondary-foreground: oklch(0.24 0.018 75);
--accent: oklch(0.97 0 0); --muted: oklch(0.925 0.012 95);
--accent-foreground: oklch(0.205 0 0); --muted-foreground: oklch(0.48 0.018 75);
--destructive: oklch(0.577 0.245 27.325); --accent: oklch(0.88 0.04 112);
--border: oklch(0.922 0 0); --accent-foreground: oklch(0.2 0.028 110);
--input: oklch(0.922 0 0); --destructive: oklch(0.57 0.205 28);
--ring: oklch(0.708 0 0); --success: oklch(0.59 0.155 132);
--chart-1: oklch(0.646 0.222 41.116); --success-foreground: oklch(0.28 0.09 132);
--chart-2: oklch(0.6 0.118 184.704); --warning: oklch(0.72 0.15 76);
--chart-3: oklch(0.398 0.07 227.392); --warning-foreground: oklch(0.34 0.09 66);
--chart-4: oklch(0.828 0.189 84.429); --border: oklch(0.79 0.018 90);
--chart-5: oklch(0.769 0.188 70.08); --input: oklch(0.76 0.02 90);
--sidebar: oklch(0.985 0 0); --ring: oklch(0.68 0.19 125);
--sidebar-foreground: oklch(0.145 0 0); --chart-1: oklch(0.75 0.205 125);
--sidebar-primary: oklch(0.205 0 0); --chart-2: oklch(0.66 0.16 205);
--sidebar-primary-foreground: oklch(0.985 0 0); --chart-3: oklch(0.58 0.18 50);
--sidebar-accent: oklch(0.97 0 0); --chart-4: oklch(0.72 0.17 80);
--sidebar-accent-foreground: oklch(0.205 0 0); --chart-5: oklch(0.55 0.16 300);
--sidebar-border: oklch(0.922 0 0); --sidebar: oklch(0.17 0.02 75);
--sidebar-ring: oklch(0.708 0 0); --sidebar-foreground: oklch(0.91 0.015 95);
--sidebar-primary: oklch(0.75 0.205 125);
--sidebar-primary-foreground: oklch(0.15 0.025 125);
--sidebar-accent: oklch(0.25 0.025 90);
--sidebar-accent-foreground: oklch(0.96 0.01 95);
--sidebar-border: oklch(0.31 0.025 80);
--sidebar-ring: oklch(0.75 0.205 125);
} }
.dark { .dark {
@@ -117,8 +129,77 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
html {
@apply bg-background;
}
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground antialiased;
min-width: 320px;
background-image:
linear-gradient(to right, color-mix(in oklch, var(--border) 22%, transparent) 1px, transparent 1px),
linear-gradient(to bottom, color-mix(in oklch, var(--border) 22%, transparent) 1px, transparent 1px);
background-size: 32px 32px;
}
h1,
h2,
h3 {
letter-spacing: -0.025em;
}
::selection {
background: var(--primary);
color: var(--primary-foreground);
}
}
@layer components {
.page-shell {
@apply mx-auto w-full max-w-[1600px] space-y-6;
}
.page-heading {
@apply flex flex-col gap-4 border-b-2 border-foreground pb-5 sm:flex-row sm:items-end sm:justify-between;
}
.page-eyebrow {
@apply mb-2 block font-mono text-[10px] font-bold uppercase tracking-[0.22em] text-muted-foreground;
}
.page-title {
@apply text-3xl font-black leading-none tracking-[-0.04em] sm:text-4xl;
}
.page-description {
@apply mt-2 max-w-2xl text-sm leading-6 text-muted-foreground;
}
.auth-grid {
background-image:
linear-gradient(to right, color-mix(in oklch, white 9%, transparent) 1px, transparent 1px),
linear-gradient(to bottom, color-mix(in oklch, white 9%, transparent) 1px, transparent 1px);
background-size: 36px 36px;
}
.server-form [data-slot="tabs-content"] {
@apply border border-border bg-background/65 p-4 sm:p-6;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@media (max-width: 767px) {
.server-form .grid-cols-2,
.server-form .grid-cols-3,
.server-form .grid-cols-4 {
grid-template-columns: minmax(0, 1fr);
} }
} }
+3 -3
View File
@@ -2,14 +2,14 @@ import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Minikura - Minecraft Server Manager", title: "Minikura | Infrastructure Console",
description: "Manage your Minecraft servers with ease", description: "Minecraft infrastructure control plane",
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body className="antialiased">{children}</body> <body>{children}</body>
</html> </html>
); );
} }
+80 -56
View File
@@ -1,13 +1,15 @@
"use client"; "use client";
import { Loader2 } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { AuthFormCard, FormError } from "@/components/auth/auth-form-card";
import { BrandLockup } from "@/components/brand";
import { FullScreenLoader } from "@/components/page-layout";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { signIn, useSession } from "@/lib/auth-client"; import { authClient, signIn, useSession } from "@/lib/auth-client";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@@ -39,7 +41,16 @@ export default function LoginPage() {
if (result.error) { if (result.error) {
setError(result.error.message || "Invalid email or password"); setError(result.error.message || "Invalid email or password");
} else { } else {
router.push("/dashboard"); const refreshedSession = await authClient.getSession({
query: { disableCookieCache: true },
});
if (!refreshedSession.data?.user) {
setError("Signed in, but the session cookie was not accepted. Check the web and API URLs.");
return;
}
window.location.assign("/dashboard");
} }
} catch (_err) { } catch (_err) {
setError("Failed to connect to server"); setError("Failed to connect to server");
@@ -48,59 +59,72 @@ export default function LoginPage() {
} }
}; };
if (isPending) { if (isPending || session?.user) return <FullScreenLoader />;
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (session?.user) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4"> <main className="grid min-h-screen bg-sidebar text-sidebar-foreground lg:grid-cols-[1.2fr_0.8fr]">
<Card className="w-full max-w-md"> <section className="relative hidden overflow-hidden border-r border-sidebar-border bg-[url('/background.png')] bg-cover bg-center p-10 lg:flex lg:flex-col lg:justify-between xl:p-16">
<CardHeader className="space-y-1"> <div className="absolute inset-0 bg-gradient-to-r from-black/75 via-black/45 to-black/20" />
<CardTitle className="text-2xl font-bold text-center">Minikura</CardTitle> <BrandLockup subtitle="Infrastructure console" className="relative z-10" />
<CardDescription className="text-center">Sign in to your account</CardDescription>
</CardHeader> <div className="relative z-10 max-w-2xl py-20">
<CardContent> <span className="mb-5 inline-flex items-center gap-2 border border-sidebar-border bg-sidebar-accent px-3 py-1.5 font-mono text-[10px] font-bold uppercase tracking-[0.18em]">
<form onSubmit={handleSubmit} className="space-y-4"> <span className="size-2 bg-sidebar-primary" /> Minecraft operations platform
<div className="space-y-2"> </span>
<Label htmlFor="email">Email</Label> <h1 className="text-6xl font-black uppercase leading-[0.86] tracking-[-0.065em] xl:text-8xl">
<Input Orchestrate
id="email" <br />
name="email" <span className="text-sidebar-primary">Every World.</span>
type="email" </h1>
placeholder="admin@example.com" <p className="mt-7 max-w-lg text-base leading-7 text-sidebar-foreground/55">
required Provision servers, manage proxy routes, and monitor Kubernetes workloads from a single
autoFocus operational interface.
/> </p>
</div> </div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label> <p className="relative z-10 font-mono text-[9px] uppercase tracking-[0.2em] text-sidebar-foreground/35">
<Input Server and cluster administration
id="password" </p>
name="password" </section>
type="password"
placeholder="••••••••" <section className="flex items-center justify-center bg-background p-5 text-foreground sm:p-10">
required <AuthFormCard
/> title="Sign In"
</div> description="Use your administrator account to access Minikura."
{error && <div className="text-sm text-red-600 text-center">{error}</div>} className="border-2 border-foreground shadow-none"
<Button type="submit" className="w-full" disabled={loading}> headerClassName="border-b"
{loading ? "Signing in..." : "Sign In"} contentClassName="pt-2"
</Button> >
</form> <form onSubmit={handleSubmit} className="space-y-5">
</CardContent> <div className="space-y-2">
</Card> <Label htmlFor="email">Email</Label>
</div> <Input
id="email"
name="email"
type="email"
placeholder="admin@example.com"
required
autoFocus
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
placeholder="Enter password"
required
/>
</div>
<FormError message={error} />
<Button type="submit" size="lg" className="w-full" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
{!loading && <ArrowRight className="ml-auto" />}
</Button>
</form>
</AuthFormCard>
</section>
</main>
); );
} }
@@ -0,0 +1,44 @@
import type * as React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/cn";
type AuthFormCardProps = {
title: string;
description: string;
children: React.ReactNode;
className?: string;
headerClassName?: string;
contentClassName?: string;
};
export function AuthFormCard({
title,
description,
children,
className,
headerClassName,
contentClassName,
}: AuthFormCardProps) {
return (
<Card className={cn("w-full max-w-md", className)}>
<CardHeader className={cn("space-y-3", headerClassName)}>
<CardTitle className="text-3xl tracking-[-0.035em]">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent className={contentClassName}>{children}</CardContent>
</Card>
);
}
export function FormError({ message }: { message?: string | null }) {
if (!message) return null;
return (
<div
role="alert"
className="border-l-4 border-destructive bg-destructive/10 px-3 py-2 text-sm text-destructive"
>
{message}
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { cn } from "@/lib/cn";
type BrandMarkProps = {
className?: string;
inverted?: boolean;
};
export function BrandMark({ className, inverted = false }: BrandMarkProps) {
return (
<div
className={cn(
"grid size-11 shrink-0 place-items-center border font-mono text-lg font-black",
inverted
? "border-foreground bg-foreground text-background"
: "border-sidebar-primary bg-sidebar-primary text-sidebar-primary-foreground",
className
)}
>
M
</div>
);
}
type BrandLockupProps = BrandMarkProps & {
subtitle: string;
compact?: boolean;
textClassName?: string;
};
export function BrandLockup({ subtitle, compact, className, textClassName }: BrandLockupProps) {
return (
<div className={cn("flex items-center gap-3", className)}>
<BrandMark className={compact ? "size-9 text-sm" : undefined} />
<div className={textClassName}>
<p className={cn("font-black uppercase tracking-[0.14em]", compact ? "text-base" : "text-lg")}>
Minikura
</p>
<p className="font-mono text-[9px] uppercase tracking-[0.2em] text-current/45">{subtitle}</p>
</div>
</div>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
type ConfirmDialogProps = {
open: boolean;
title: string;
description: React.ReactNode;
confirmLabel: string;
onConfirm: () => void | Promise<void>;
onOpenChange: (open: boolean) => void;
};
export function ConfirmDialog({
open,
title,
description,
confirmLabel,
onConfirm,
onOpenChange,
}: ConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={onConfirm}>
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+89 -78
View File
@@ -1,16 +1,17 @@
"use client"; "use client";
import { GitGraph, Loader2, LogOut, Network, Server, Settings, Users } from "lucide-react"; import { GitGraph, LogOut, type LucideIcon, Network, Server, Users } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { BrandLockup } from "@/components/brand";
import { FullScreenLoader } from "@/components/page-layout";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import {
@@ -29,13 +30,30 @@ import {
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { signOut, useSession } from "@/lib/auth-client"; import { signOut, useSession } from "@/lib/auth-client";
const menuItems = [ type NavigationGroup = {
{ href: "/dashboard/users", icon: Users, label: "Users" }, label: string;
{ href: "/dashboard/servers", icon: Server, label: "Servers" }, items: Array<{
{ href: "/dashboard/topology", icon: GitGraph, label: "Network" }, href: string;
]; icon: LucideIcon;
label: string;
context: string;
}>;
};
const k8sMenuItems = [{ href: "/dashboard/k8s", icon: Network, label: "Resources" }]; const navigation: NavigationGroup[] = [
{
label: "Operations",
items: [
{ href: "/dashboard/users", icon: Users, label: "Users", context: "Identity" },
{ href: "/dashboard/servers", icon: Server, label: "Servers", context: "Workloads" },
{ href: "/dashboard/topology", icon: GitGraph, label: "Network", context: "Topology" },
],
},
{
label: "Kubernetes",
items: [{ href: "/dashboard/k8s", icon: Network, label: "Resources", context: "Cluster" }],
},
];
export function DashboardLayout({ children }: { children: React.ReactNode }) { export function DashboardLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
@@ -48,21 +66,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
} }
}, [session, isPending, router]); }, [session, isPending, router]);
if (isPending) { if (isPending || !session?.user) return <FullScreenLoader />;
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!session?.user) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
const handleSignOut = async () => { const handleSignOut = async () => {
await signOut(); await signOut();
@@ -75,69 +79,73 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
.map((n) => n[0]) .map((n) => n[0])
.join("") .join("")
.toUpperCase() || "U"; .toUpperCase() || "U";
const currentPage = navigation
.flatMap((group) => group.items)
.find((item) => pathname === item.href || pathname.startsWith(`${item.href}/`));
return ( return (
<SidebarProvider> <SidebarProvider
<Sidebar> style={
<SidebarHeader className="border-b px-6 py-4"> {
<h2 className="text-lg font-semibold">Minikura</h2> "--sidebar-width": "17rem",
"--sidebar-width-icon": "4rem",
} as React.CSSProperties
}
>
<Sidebar className="border-sidebar-border">
<SidebarHeader className="border-b border-sidebar-border px-5 py-5">
<BrandLockup
subtitle="Control plane"
compact
textClassName="group-data-[collapsible=icon]:hidden"
/>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent className="py-4">
<SidebarGroup> {navigation.map((group) => (
<SidebarGroupContent> <SidebarGroup key={group.label} className="px-3">
<SidebarMenu> <SidebarGroupLabel className="font-mono text-[9px] uppercase tracking-[0.2em]">
{menuItems.map((item) => ( {group.label}
<SidebarMenuItem key={item.href}> </SidebarGroupLabel>
<SidebarMenuButton asChild isActive={pathname === item.href}> <SidebarGroupContent>
<Link href={item.href}> <SidebarMenu>
<item.icon className="h-4 w-4" /> {group.items.map((item) => (
<span>{item.label}</span> <SidebarMenuItem key={item.href}>
</Link> <SidebarMenuButton
</SidebarMenuButton> asChild
</SidebarMenuItem> isActive={pathname === item.href || pathname.startsWith(`${item.href}/`)}
))} className="h-11 rounded-sm px-3 text-sm font-bold data-[active=true]:border-l-2 data-[active=true]:border-sidebar-primary"
</SidebarMenu> >
</SidebarGroupContent> <Link href={item.href}>
</SidebarGroup> <item.icon className="h-4 w-4" />
<SidebarGroup> <span>{item.label}</span>
<SidebarGroupLabel>Kubernetes</SidebarGroupLabel> </Link>
<SidebarGroupContent> </SidebarMenuButton>
<SidebarMenu> </SidebarMenuItem>
{k8sMenuItems.map((item) => ( ))}
<SidebarMenuItem key={item.href}> </SidebarMenu>
<SidebarMenuButton asChild isActive={pathname === item.href}> </SidebarGroupContent>
<Link href={item.href}> </SidebarGroup>
<item.icon className="h-4 w-4" /> ))}
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent> </SidebarContent>
<div className="border-t p-4"> <div className="border-t border-sidebar-border p-3">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2 px-2"> <Button
<Avatar className="h-8 w-8"> variant="ghost"
className="h-auto w-full justify-start gap-3 overflow-hidden px-2 py-2 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-sm">
<AvatarFallback>{userInitials}</AvatarFallback> <AvatarFallback>{userInitials}</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col items-start text-sm"> <div className="flex min-w-0 flex-col items-start normal-case tracking-normal group-data-[collapsible=icon]:hidden">
<span className="font-medium">{session?.user?.name}</span> <span className="max-w-40 truncate text-xs font-bold">{session?.user?.name}</span>
<span className="text-xs text-muted-foreground">{session?.user?.email}</span> <span className="max-w-40 truncate font-mono text-[9px] text-sidebar-foreground/45">
{session?.user?.email}
</span>
</div> </div>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem asChild>
<Link href="/dashboard/settings">
<Settings className="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut}> <DropdownMenuItem onClick={handleSignOut}>
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
Sign Out Sign Out
@@ -147,11 +155,14 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
</div> </div>
</Sidebar> </Sidebar>
<SidebarInset> <SidebarInset>
<header className="flex h-16 items-center gap-4 border-b bg-background px-6"> <header className="sticky top-0 z-20 flex h-14 items-center gap-4 border-b bg-background/95 px-4 backdrop-blur-sm sm:px-6">
<SidebarTrigger /> <SidebarTrigger className="border border-border bg-card" />
<div className="flex-1" /> <div className="h-5 w-px bg-border" />
<span className="truncate font-mono text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
{currentPage ? `${currentPage.context} / ${currentPage.label}` : "Control plane"}
</span>
</header> </header>
<main className="flex-1 p-6 min-w-0 overflow-auto">{children}</main> <main className="min-w-0 flex-1 overflow-auto p-4 sm:p-6 lg:p-8">{children}</main>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
); );
+50
View File
@@ -0,0 +1,50 @@
import type { ReactNode } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export type DataTableColumn<T> = {
id: string;
header: ReactNode;
cell: (item: T) => ReactNode;
className?: string;
headerClassName?: string;
};
type DataTableProps<T> = {
data: readonly T[];
columns: readonly DataTableColumn<T>[];
getRowKey: (item: T, index: number) => string | number;
};
export function DataTable<T>({ data, columns, getRowKey }: DataTableProps<T>) {
return (
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead key={column.id} className={column.headerClassName}>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.map((item, index) => (
<TableRow key={getRowKey(item, index)}>
{columns.map((column) => (
<TableCell key={column.id} className={column.className}>
{column.cell(item)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
+93
View File
@@ -0,0 +1,93 @@
import { Loader2 } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/cn";
type PageHeaderProps = {
eyebrow: string;
title: React.ReactNode;
description?: React.ReactNode;
leading?: React.ReactNode;
actions?: React.ReactNode;
className?: string;
};
export function PageShell({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("page-shell", className)} {...props} />;
}
export function PageHeader({
eyebrow,
title,
description,
leading,
actions,
className,
}: PageHeaderProps) {
return (
<header className={cn("page-heading", className)}>
<div className={cn(leading && "flex items-center gap-4")}>
{leading}
<div>
<span className="page-eyebrow">{eyebrow}</span>
<h1 className="page-title">{title}</h1>
{description && <p className="page-description">{description}</p>}
</div>
</div>
{actions}
</header>
);
}
type StatePanelProps = React.ComponentProps<"div"> & {
title: React.ReactNode;
description?: React.ReactNode;
icon?: React.ReactNode;
action?: React.ReactNode;
tone?: "default" | "error";
loading?: boolean;
};
export function StatePanel({
title,
description,
icon,
action,
tone = "default",
loading = false,
className,
...props
}: StatePanelProps) {
return (
<div
className={cn(
"flex min-h-48 items-center justify-center border border-dashed bg-card/60 p-6 text-center",
className
)}
{...props}
>
<div className="flex max-w-lg flex-col items-center gap-2">
{loading ? (
<Loader2 className="mb-2 size-7 animate-spin text-muted-foreground" />
) : (
icon && <div className="mb-2 text-muted-foreground">{icon}</div>
)}
<p className={cn("font-bold", tone === "error" && "text-destructive")}>{title}</p>
{description && <div className="text-sm text-muted-foreground">{description}</div>}
{action && <div className="mt-3">{action}</div>}
</div>
</div>
);
}
export function FullScreenLoader({ label }: { label?: string }) {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 bg-sidebar text-sidebar-foreground">
<Loader2 className="size-8 animate-spin text-sidebar-primary" />
{label && (
<span className="font-mono text-[10px] font-bold uppercase tracking-widest text-sidebar-foreground/50">
{label}
</span>
)}
</div>
);
}
+77
View File
@@ -0,0 +1,77 @@
import type * as React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/cn";
type SectionCardProps = React.ComponentProps<typeof Card> & {
title: React.ReactNode;
description?: React.ReactNode;
icon?: React.ReactNode;
headerAction?: React.ReactNode;
contentClassName?: string;
};
export function SectionCard({
title,
description,
icon,
headerAction,
children,
className,
contentClassName,
...props
}: SectionCardProps) {
return (
<Card className={className} {...props}>
<CardHeader className="border-b">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
{icon}
<CardTitle>{title}</CardTitle>
</div>
{headerAction}
</div>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent className={contentClassName}>{children}</CardContent>
</Card>
);
}
type ResourceSectionProps = SectionCardProps & {
count: number;
emptyIcon: React.ReactNode;
emptyTitle: string;
emptyDescription: string;
};
export function ResourceSection({
count,
emptyIcon,
emptyTitle,
emptyDescription,
children,
...props
}: ResourceSectionProps) {
return (
<SectionCard
{...props}
headerAction={
<span className="font-mono text-xs font-bold">{String(count).padStart(2, "0")}</span>
}
>
{count === 0 ? (
<div className="flex h-36 flex-col items-center justify-center border border-dashed bg-muted/30 text-center">
<div className="mb-3 text-muted-foreground">{emptyIcon}</div>
<p className="font-bold uppercase">{emptyTitle}</p>
<p className="mt-1 text-xs text-muted-foreground">{emptyDescription}</p>
</div>
) : (
children
)}
</SectionCard>
);
}
export function TableActions({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("flex items-center justify-end gap-2", className)} {...props} />;
}
+47
View File
@@ -0,0 +1,47 @@
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/cn";
export type StatItem = {
label: string;
value: number | string;
icon?: LucideIcon;
tone?: "default" | "positive" | "warning";
};
export function StatStrip({
items,
className,
}: {
items: readonly StatItem[];
className?: string;
}) {
return (
<div
className={cn(
"grid divide-x divide-border border bg-card shadow-[2px_2px_0_color-mix(in_oklch,var(--foreground)_8%,transparent)]",
className
)}
style={{ gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))` }}
>
{items.map(({ label, value, icon: Icon, tone = "default" }) => (
<div key={label} className="min-w-20 px-3 py-2.5 sm:min-w-28 sm:px-4">
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
{Icon && (
<Icon
className={cn(
"size-3.5",
tone === "positive" && "text-success",
tone === "warning" && "text-warning"
)}
/>
)}
<span className="truncate font-mono text-[9px] font-bold uppercase tracking-[0.12em]">
{label}
</span>
</div>
<strong className="block text-xl leading-none tabular-nums sm:text-2xl">{value}</strong>
</div>
))}
</div>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { AlertCircle, CheckCircle2, CircleDashed, XCircle } from "lucide-react";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/cn";
const statusConfig = {
success: {
icon: CheckCircle2,
className: "border-success/35 bg-success/12 text-success-foreground",
},
warning: {
icon: AlertCircle,
className: "border-warning/40 bg-warning/14 text-warning-foreground",
},
error: { icon: XCircle, className: "border-destructive/35 bg-destructive/10 text-destructive" },
neutral: { icon: CircleDashed, className: "border-border bg-muted/60 text-muted-foreground" },
} as const;
export type StatusTone = keyof typeof statusConfig;
export function StatusBadge({
tone = "neutral",
children,
pulse = false,
className,
}: {
tone?: StatusTone;
children: ReactNode;
pulse?: boolean;
className?: string;
}) {
const { icon: Icon, className: toneClassName } = statusConfig[tone];
return (
<Badge variant="outline" className={cn(toneClassName, className)}>
<Icon className={cn(pulse && "animate-pulse")} />
{children}
</Badge>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", "flex flex-1 items-center justify-between py-4 font-mono text-[10px] font-bold uppercase tracking-[0.1em] transition-colors hover:text-primary [&[data-state=open]>svg]:rotate-180",
className className
)} )}
{...props} {...props}
+2 -2
View File
@@ -5,11 +5,11 @@ import type * as React from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border px-2 py-0.5 font-mono text-[10px] font-bold uppercase tracking-[0.08em] whitespace-nowrap transition-[color,box-shadow] [&>svg]:size-3 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", default: "border-foreground/20 bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive: destructive:
+11 -11
View File
@@ -5,24 +5,24 @@ import type * as React from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md 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 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-sm border border-transparent text-sm font-bold tracking-[-0.01em] transition-[background-color,color,border-color,transform,box-shadow] disabled:pointer-events-none disabled:opacity-50 active:translate-y-px [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/40 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default:
"border-foreground bg-primary text-primary-foreground shadow-[3px_3px_0_var(--foreground)] hover:-translate-y-0.5 hover:shadow-[4px_4px_0_var(--foreground)]",
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "border-destructive bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20",
outline: outline: "border-border bg-card text-foreground hover:border-foreground hover:bg-accent",
"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: "border-border bg-secondary text-secondary-foreground hover:border-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "text-current hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
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-10 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-12 px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-10",
"icon-sm": "size-8", "icon-sm": "size-8",
"icon-lg": "size-10", "icon-lg": "size-10",
}, },
+5 -5
View File
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-5 rounded-sm border py-5 shadow-[3px_3px_0_color-mix(in_oklch,var(--foreground)_8%,transparent)]",
className className
)} )}
{...props} {...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-5 sm:px-6",
className className
)} )}
{...props} {...props}
@@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn("text-base font-black leading-none tracking-[-0.015em]", className)}
{...props} {...props}
/> />
); );
@@ -59,7 +59,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />; return <div data-slot="card-content" className={cn("px-5 sm:px-6", className)} {...props} />;
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -72,4 +72,4 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
); );
} }
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
+2 -2
View File
@@ -52,7 +52,7 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", "bg-card data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-5 rounded-sm border-2 border-foreground p-6 shadow-[8px_8px_0_color-mix(in_oklch,var(--foreground)_22%,transparent)] duration-200 outline-none sm:max-w-lg",
className className
)} )}
{...props} {...props}
@@ -96,7 +96,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-xl font-black uppercase leading-none tracking-[-0.02em]", className)}
{...props} {...props}
/> />
); );
+2 -2
View File
@@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base 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 border-input h-10 w-full min-w-0 rounded-sm border bg-card px-3 py-1 font-mono text-base 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]", "focus-visible:border-foreground focus-visible:shadow-[inset_3px_0_0_var(--primary)]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className
)} )}
+1 -1
View File
@@ -10,7 +10,7 @@ function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimiti
<LabelPrimitive.Root <LabelPrimitive.Root
data-slot="label" data-slot="label"
className={cn( className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", "flex items-center gap-2 font-mono text-[10px] font-bold uppercase leading-none tracking-[0.08em] select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className className
)} )}
{...props} {...props}
+2 -2
View File
@@ -31,7 +31,7 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( 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 rounded-md 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-foreground aria-invalid:ring-destructive/20 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-sm border bg-card px-3 py-2 font-mono text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:shadow-[inset_3px_0_0_var(--primary)] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 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 className
)} )}
{...props} {...props}
@@ -56,7 +56,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-sm border shadow-[4px_4px_0_color-mix(in_oklch,var(--foreground)_15%,transparent)]",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
-4
View File
@@ -18,8 +18,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_WIDTH_ICON = "3rem";
@@ -70,8 +68,6 @@ function SidebarProvider({
} else { } else {
_setOpen(openState); _setOpen(openState);
} }
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open] [setOpenProp, open]
); );
+3 -3
View File
@@ -45,7 +45,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "hover:bg-primary/8 data-[state=selected]:bg-muted border-b transition-colors",
className className
)} )}
{...props} {...props}
@@ -58,7 +58,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "h-10 px-3 text-left align-middle font-mono text-[10px] font-bold uppercase tracking-[0.14em] text-muted-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className
)} )}
{...props} {...props}
@@ -71,7 +71,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "h-14 px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className
)} )}
{...props} {...props}
+2 -2
View File
@@ -20,7 +20,7 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
<TabsPrimitive.List <TabsPrimitive.List
data-slot="tabs-list" data-slot="tabs-list"
className={cn( className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", "text-muted-foreground inline-flex h-auto w-fit items-center justify-start gap-1 overflow-x-auto border-b-2 border-foreground bg-transparent p-0",
className className
)} )}
{...props} {...props}
@@ -33,7 +33,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[state=active]:bg-foreground data-[state=active]:text-background focus-visible:ring-ring/50 text-muted-foreground inline-flex h-9 flex-none items-center justify-center gap-1.5 rounded-t-sm border border-b-0 border-transparent px-3 py-1 font-mono text-[10px] font-bold uppercase tracking-[0.1em] whitespace-nowrap transition-colors focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex min-h-[80px] w-full rounded-sm border border-input bg-card px-3 py-2 font-mono text-sm placeholder:text-muted-foreground focus-visible:border-foreground focus-visible:shadow-[inset_3px_0_0_var(--primary)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
ref={ref} ref={ref}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB