🎨 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
@@ -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";
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 { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react";
import { BrandLockup } from "@/components/brand";
import { FullScreenLoader } from "@/components/page-layout";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -29,13 +30,30 @@ import {
} from "@/components/ui/sidebar";
import { signOut, useSession } from "@/lib/auth-client";
const menuItems = [
{ href: "/dashboard/users", icon: Users, label: "Users" },
{ href: "/dashboard/servers", icon: Server, label: "Servers" },
{ href: "/dashboard/topology", icon: GitGraph, label: "Network" },
];
type NavigationGroup = {
label: string;
items: Array<{
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 }) {
const pathname = usePathname();
@@ -48,21 +66,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
}
}, [session, isPending, router]);
if (isPending) {
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>
);
}
if (isPending || !session?.user) return <FullScreenLoader />;
const handleSignOut = async () => {
await signOut();
@@ -75,69 +79,73 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
.map((n) => n[0])
.join("")
.toUpperCase() || "U";
const currentPage = navigation
.flatMap((group) => group.items)
.find((item) => pathname === item.href || pathname.startsWith(`${item.href}/`));
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader className="border-b px-6 py-4">
<h2 className="text-lg font-semibold">Minikura</h2>
<SidebarProvider
style={
{
"--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>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{menuItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={pathname === item.href}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Kubernetes</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{k8sMenuItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={pathname === item.href}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarContent className="py-4">
{navigation.map((group) => (
<SidebarGroup key={group.label} className="px-3">
<SidebarGroupLabel className="font-mono text-[9px] uppercase tracking-[0.2em]">
{group.label}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
asChild
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"
>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<div className="border-t p-4">
<div className="border-t border-sidebar-border p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2 px-2">
<Avatar className="h-8 w-8">
<Button
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>
</Avatar>
<div className="flex flex-col items-start text-sm">
<span className="font-medium">{session?.user?.name}</span>
<span className="text-xs text-muted-foreground">{session?.user?.email}</span>
<div className="flex min-w-0 flex-col items-start normal-case tracking-normal group-data-[collapsible=icon]:hidden">
<span className="max-w-40 truncate text-xs font-bold">{session?.user?.name}</span>
<span className="max-w-40 truncate font-mono text-[9px] text-sidebar-foreground/45">
{session?.user?.email}
</span>
</div>
</Button>
</DropdownMenuTrigger>
<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}>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
@@ -147,11 +155,14 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
</div>
</Sidebar>
<SidebarInset>
<header className="flex h-16 items-center gap-4 border-b bg-background px-6">
<SidebarTrigger />
<div className="flex-1" />
<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 className="border border-border bg-card" />
<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>
<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>
</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
ref={ref}
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
)}
{...props}
+2 -2
View File
@@ -5,11 +5,11 @@ import type * as React from "react";
import { cn } from "@/lib/cn";
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: {
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:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
+11 -11
View File
@@ -5,24 +5,24 @@ import type * as React from "react";
import { cn } from "@/lib/cn";
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: {
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:
"bg-destructive text-white 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 hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"border-destructive bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20",
outline: "border-border bg-card text-foreground hover:border-foreground hover:bg-accent",
secondary: "border-border bg-secondary text-secondary-foreground hover:border-foreground",
ghost: "text-current 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: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: "h-10 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-12 px-6 has-[>svg]:px-4",
icon: "size-10",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
+5 -5
View File
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
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
)}
{...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
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
)}
{...props}
@@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
className={cn("text-base font-black leading-none tracking-[-0.015em]", className)}
{...props}
/>
);
@@ -59,7 +59,7 @@ function CardAction({ 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">) {
@@ -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
data-slot="dialog-content"
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
)}
{...props}
@@ -96,7 +96,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
return (
<DialogPrimitive.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}
/>
);
+2 -2
View File
@@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input 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",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"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-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",
className
)}
+1 -1
View File
@@ -10,7 +10,7 @@ function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimiti
<LabelPrimitive.Root
data-slot="label"
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
)}
{...props}
+2 -2
View File
@@ -31,7 +31,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 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
)}
{...props}
@@ -56,7 +56,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
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" &&
"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
-4
View File
@@ -18,8 +18,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
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_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
@@ -70,8 +68,6 @@ function SidebarProvider({
} else {
_setOpen(openState);
}
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
+3 -3
View File
@@ -45,7 +45,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr
data-slot="table-row"
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
)}
{...props}
@@ -58,7 +58,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
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
)}
{...props}
@@ -71,7 +71,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td
data-slot="table-cell"
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
)}
{...props}
+2 -2
View File
@@ -20,7 +20,7 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
<TabsPrimitive.List
data-slot="tabs-list"
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
)}
{...props}
@@ -33,7 +33,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
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
)}
{...props}
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
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
)}
ref={ref}