Apply linen design system across app

This commit is contained in:
t.indorf
2026-05-20 22:47:52 +02:00
parent 99ff978116
commit 1c5d2f10ac
56 changed files with 4698 additions and 2159 deletions
+46 -26
View File
@@ -1,6 +1,5 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
@@ -15,24 +14,28 @@ import {
Megaphone,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { NavItem } from "@/components/ui/nav-item";
import { UserRole } from "@prisma/client";
const navItems = [
{
section: "Heute",
href: "/dashboard",
label: "Übersicht",
icon: LayoutDashboard,
exact: true,
},
{
section: "Heute",
href: "/dashboard/notdienst",
label: "Notdienst (Eltern)",
icon: CalendarCheck2,
exact: true,
badge: "4",
},
{
section: "Heute",
href: "/dashboard/notdienst/plan",
label: "Notdienst-Planung",
icon: CalendarCheck2,
@@ -40,18 +43,21 @@ const navItems = [
allowedRoles: [UserRole.ADMIN, UserRole.KOORDINATOR],
},
{
section: "Heute",
href: "/dashboard/kalender",
label: "Terminkalender",
icon: CalendarDays,
exact: false,
},
{
section: "Heute",
href: "/dashboard/adressbuch",
label: "Adressbuch",
icon: Contact,
exact: false,
},
{
section: "Verwaltung",
href: "/dashboard/families",
label: "Familienverwaltung",
icon: Users,
@@ -59,6 +65,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN],
},
{
section: "Verwaltung",
href: "/dashboard/admin/dienste",
label: "Dienstplan",
icon: ClipboardList,
@@ -66,6 +73,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN],
},
{
section: "Verwaltung",
href: "/dashboard/admin/news",
label: "Schwarzes Brett",
icon: Megaphone,
@@ -73,6 +81,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN],
},
{
section: "Verwaltung",
href: "/dashboard/admin/kalender",
label: "Event-Anmeldungen",
icon: CalendarCheck2,
@@ -80,6 +89,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN, UserRole.KOORDINATOR],
},
{
section: "Verwaltung",
href: "/dashboard/admin/abwesenheiten",
label: "Abwesenheiten",
icon: Stethoscope,
@@ -87,6 +97,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN, UserRole.KOORDINATOR, UserRole.ERZIEHER],
},
{
section: "Verwaltung",
href: "/dashboard/erzieher",
label: "ErzieherInnen",
icon: GraduationCap,
@@ -94,6 +105,7 @@ const navItems = [
allowedRoles: [UserRole.ADMIN],
},
{
section: "Verwaltung",
href: "/dashboard/profil",
label: "Mein Profil",
icon: UserCircle,
@@ -103,32 +115,40 @@ const navItems = [
export function SidebarNav({ role }: { role: UserRole }) {
const pathname = usePathname();
const visibleItems = navItems.filter(
({ allowedRoles }) =>
!allowedRoles || (allowedRoles as UserRole[]).includes(role),
);
const sections = ["Heute", "Verwaltung"];
return (
<nav className="flex flex-col gap-1 px-3">
{navItems.map(({ href, label, icon: Icon, exact, allowedRoles }) => {
// Filter out items not allowed for the current role
if (allowedRoles && !(allowedRoles as UserRole[]).includes(role)) {
return null;
}
const isActive = exact ? pathname === href : pathname.startsWith(href);
return (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)}
>
<Icon className="h-4 w-4 shrink-0" />
{label}
</Link>
);
})}
<nav className="flex flex-col gap-8 px-5">
{sections.map((section) => (
<div key={section}>
<p className="mb-3 px-2.5 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.18em]">
{section}
</p>
<div className="flex flex-col gap-1">
{visibleItems
.filter((item) => item.section === section)
.map(({ href, label, icon, exact, badge }) => {
const isActive = exact
? pathname === href
: pathname.startsWith(href);
return (
<NavItem
key={href}
href={href}
label={label}
icon={icon}
active={isActive}
badge={badge}
/>
);
})}
</div>
</div>
))}
</nav>
);
}
+7 -7
View File
@@ -4,21 +4,21 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-[11px] font-semibold transition-colors focus:outline-none focus:ring-[3px] focus:ring-[var(--focus-ring)]",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground",
"border-transparent bg-[var(--accent-deep)] text-[oklch(0.98_0.005_45)]",
secondary:
"border-transparent bg-secondary text-secondary-foreground",
"border-[var(--hairline)] bg-[var(--surface-2)] text-[var(--ink-soft)]",
destructive:
"border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
"border-transparent bg-[var(--danger-soft)] text-[var(--danger)]",
outline: "border-[var(--hairline)] text-[var(--ink-soft)]",
success:
"border-transparent bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
"border-transparent bg-[var(--good-soft)] text-[var(--good)]",
warning:
"border-transparent bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
"border-transparent bg-[var(--warn-soft)] text-[var(--warn)]",
},
},
defaultVariants: {
+14 -10
View File
@@ -5,24 +5,28 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap text-[13.5px] font-semibold transition focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)] disabled:pointer-events-none disabled:opacity-50 active:translate-y-px [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.7]",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default:
"rounded-lg bg-[var(--accent-deep)] text-[oklch(0.98_0.005_45)] hover:bg-[oklch(0.36_0.10_38)]",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
"rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] text-[var(--ink)] hover:border-[var(--accent-mid)]",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
"rounded-lg border border-[var(--hairline)] bg-transparent text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
ghost:
"rounded-lg border border-[var(--hairline)] bg-transparent text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
"rounded-lg bg-[var(--danger)] text-[oklch(0.98_0.005_45)] hover:brightness-95",
link: "rounded-lg text-[var(--accent-deep)] underline-offset-4 hover:underline",
pill:
"rounded-full border border-[var(--hairline)] bg-transparent text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
default: "h-10 gap-2 px-[18px] py-2.5",
sm: "h-9 gap-2 px-3",
lg: "h-11 gap-2 px-8",
icon: "h-10 w-10",
},
},
+29 -11
View File
@@ -1,15 +1,33 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
const cardVariants = cva(
"relative rounded-[14px] border border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink)]",
{
variants: {
variant: {
default: "",
featured:
"overflow-hidden border-[var(--accent-mid)] bg-linear-to-b from-[oklch(0.985_0.012_50)] to-[var(--surface)] before:absolute before:inset-x-0 before:top-0 before:h-0.5 before:bg-[var(--accent)]",
},
},
defaultVariants: {
variant: "default",
},
},
);
interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, variant, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
className={cn(cardVariants({ variant, className }))}
{...props}
/>
),
@@ -18,7 +36,7 @@ Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-[22px_24px]", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
@@ -31,7 +49,7 @@ const CardTitle = React.forwardRef<HTMLElement, CardTitleProps>(
({ className, as: Comp = "div", ...props }, ref) => (
<Comp
ref={ref as React.Ref<HTMLDivElement>}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
className={cn("text-lg font-medium leading-none tracking-[-0.02em]", className)}
{...props}
/>
),
@@ -42,7 +60,7 @@ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-[13.5px] text-[var(--ink-soft)]", className)}
{...props}
/>
),
@@ -51,14 +69,14 @@ CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn("p-[22px_24px] pt-0", className)} {...props} />
),
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn("flex items-center p-[22px_24px] pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
+52
View File
@@ -0,0 +1,52 @@
import type { ComponentType, ReactNode } from "react";
import type { LucideProps } from "lucide-react";
import { cn } from "@/lib/utils";
export function DefList({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<dl className={cn("divide-y divide-[var(--hairline-soft)]", className)}>
{children}
</dl>
);
}
export function DefItem({
icon: Icon,
label,
value,
}: {
icon?: ComponentType<LucideProps>;
label: string;
value: ReactNode;
}) {
return (
<div className="grid gap-1 py-3 first:pt-0 last:pb-0">
<dt className="flex items-center gap-1.5 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
{Icon ? <Icon className="h-3.5 w-3.5 [stroke-width:1.7]" /> : null}
{label}
</dt>
<dd className="text-sm font-medium text-[var(--ink)] [overflow-wrap:anywhere] [font-variant-numeric:tabular-nums]">
{value}
</dd>
</div>
);
}
export function EmptyDefValue({
children = "Noch nicht hinterlegt",
}: {
children?: ReactNode;
}) {
return (
<span className="font-normal italic text-[var(--ink-faint)]">
{children}
</span>
);
}
+5 -5
View File
@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-[oklch(0.25_0.015_45_/_0.20)] backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
@@ -35,13 +35,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-[var(--hairline)] bg-[var(--surface)] p-6 text-[var(--ink)] duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-[14px]",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-md text-[var(--ink-muted)] opacity-80 transition-opacity hover:bg-[var(--surface-2)] hover:text-[var(--ink)] hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface)] disabled:pointer-events-none data-[state=open]:bg-[var(--accent-soft)] data-[state=open]:text-[var(--accent-deep)]">
<X className="h-4 w-4" />
<span className="sr-only">Schließen</span>
</DialogPrimitive.Close>
@@ -85,7 +85,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
"text-lg font-medium leading-none tracking-[-0.02em] text-[var(--ink)]",
className,
)}
{...props}
@@ -99,7 +99,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-[var(--ink-soft)]", className)}
{...props}
/>
));
+1 -1
View File
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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 h-10 w-full rounded-lg border border-[var(--hairline)] bg-[var(--surface)] px-3 py-2.5 text-[13.5px] text-[var(--ink)] transition file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[var(--ink-faint)] hover:border-[var(--accent-mid)] focus-visible:border-[var(--accent)] focus-visible:bg-[oklch(0.99_0.005_75)] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+41
View File
@@ -0,0 +1,41 @@
import Link from "next/link";
import type { ComponentType } from "react";
import type { LucideProps } from "lucide-react";
import { cn } from "@/lib/utils";
type NavItemProps = {
href: string;
label: string;
icon: ComponentType<LucideProps>;
active?: boolean;
badge?: number | string;
};
export function NavItem({
href,
label,
icon: Icon,
active = false,
badge,
}: NavItemProps) {
return (
<Link
href={href}
className={cn(
"relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[13.5px] font-medium text-[var(--ink-soft)] transition focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)]",
"hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
active &&
"bg-[var(--accent-soft)] font-semibold text-[var(--accent-deep)] before:absolute before:left-[-14px] before:top-1.5 before:h-[calc(100%-12px)] before:w-[3px] before:rounded-full before:bg-[var(--accent)]",
)}
>
<Icon className="h-4 w-4 shrink-0 [stroke-width:1.7]" />
<span className="min-w-0 flex-1 truncate">{label}</span>
{badge ? (
<span className="rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-2 py-0.5 text-[11px] font-semibold text-[var(--ink-muted)]">
{badge}
</span>
) : null}
</Link>
);
}
+29
View File
@@ -0,0 +1,29 @@
import type { UserRole } from "@prisma/client";
import { cn } from "@/lib/utils";
type RoleBadgeProps = {
role: UserRole;
className?: string;
};
const roleLabels: Record<string, string> = {
SUPERADMIN: "Admin",
ADMIN: "Vorstand",
KOORDINATOR: "Koordinator",
ELTERN: "Eltern",
ERZIEHER: "Erzieher",
};
export function RoleBadge({ role, className }: RoleBadgeProps) {
return (
<span
className={cn(
"inline-flex w-fit rounded-full border border-[var(--accent-mid)] bg-[var(--accent-soft)] px-2.5 py-1 text-[10.5px] font-semibold uppercase text-[var(--accent-deep)] [letter-spacing:0.1em]",
className,
)}
>
{roleLabels[role] ?? role}
</span>
);
}
+11 -7
View File
@@ -16,16 +16,20 @@ const SelectTrigger = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
{...props}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex h-10 w-full appearance-none items-center justify-between rounded-lg border border-[var(--hairline)] bg-[var(--surface)] bg-[right_12px_center] bg-no-repeat px-3 py-2.5 pr-9 text-[13.5px] text-[var(--ink)] transition placeholder:text-[var(--ink-faint)] hover:border-[var(--accent-mid)] focus:border-[var(--accent)] focus:bg-[oklch(0.99_0.005_75)] focus:outline-none focus:ring-[3px] focus:ring-[var(--focus-ring)] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
style={{
backgroundImage:
"linear-gradient(45deg, transparent 50%, var(--ink-muted) 50%), linear-gradient(135deg, var(--ink-muted) 50%, transparent 50%)",
backgroundPosition: "calc(100% - 16px) 50%, calc(100% - 11px) 50%",
backgroundSize: "5px 5px, 5px 5px",
...props.style,
}}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
@@ -66,7 +70,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink)] shadow-sm 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",
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,
@@ -97,7 +101,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-[13.5px] outline-none focus:bg-[var(--accent-soft)] focus:text-[var(--accent-deep)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
+5 -4
View File
@@ -12,15 +12,16 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
position="bottom-center"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
"group toast group-[.toaster]:rounded-[10px] group-[.toaster]:border-0 group-[.toaster]:bg-[oklch(0.22_0.015_45)] group-[.toaster]:px-[18px] group-[.toaster]:py-3 group-[.toaster]:text-white group-[.toaster]:shadow-none group-data-[state=open]:animate-in group-data-[state=open]:slide-in-from-bottom-8 group-data-[state=open]:duration-200",
description: "group-[.toast]:text-white/70",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
"group-[.toast]:bg-[var(--accent)] group-[.toast]:text-white",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
"group-[.toast]:bg-white/10 group-[.toast]:text-white",
},
}}
{...props}
+35
View File
@@ -0,0 +1,35 @@
import { cn } from "@/lib/utils";
type StatProps = {
label: string;
value: number | string;
suffix?: string;
hint?: string;
className?: string;
};
export function Stat({ label, value, suffix, hint, className }: StatProps) {
return (
<div
className={cn(
"relative rounded-[14px] border border-[var(--hairline)] bg-[var(--surface)] p-5 text-[var(--ink)] before:absolute before:inset-x-5 before:top-0 before:h-[1.5px] before:bg-[var(--accent)] before:opacity-40",
className,
)}
>
<p className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
{label}
</p>
<p className="mt-2 text-[30px] font-medium leading-none tracking-[-0.025em] [font-variant-numeric:tabular-nums]">
{value}
{suffix ? (
<em className="ml-1 text-sm not-italic text-[var(--ink-muted)]">
{suffix}
</em>
) : null}
</p>
{hint ? (
<p className="mt-2 text-xs text-[var(--ink-soft)]">{hint}</p>
) : null}
</div>
);
}
+16 -6
View File
@@ -9,7 +9,10 @@ const Table = React.forwardRef<
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn(
"w-full caption-bottom text-[13.5px] text-[var(--ink)]",
className,
)}
{...props}
/>
</div>
@@ -20,7 +23,11 @@ const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
<thead
ref={ref}
className={cn("[&_tr]:border-b [&_tr]:border-[var(--hairline-soft)]", className)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
@@ -43,7 +50,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
"border-b border-[var(--hairline-soft)] transition-colors hover:bg-[var(--surface-2)] data-[state=selected]:bg-[var(--accent-soft)]",
className,
)}
{...props}
@@ -58,7 +65,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle text-xs font-medium uppercase tracking-wide text-muted-foreground [&:has([role=checkbox])]:pr-0",
"h-11 px-4 text-left align-middle text-[11px] font-semibold uppercase tracking-[0.12em] text-[var(--ink-muted)] [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
@@ -72,7 +79,10 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("px-4 py-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
"px-4 py-3.5 align-middle text-[var(--ink)] [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
@@ -84,7 +94,7 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
className={cn("mt-4 text-sm text-[var(--ink-muted)]", className)}
{...props}
/>
));
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
<textarea
ref={ref}
className={cn(
"flex min-h-24 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-24 w-full rounded-lg border border-[var(--hairline)] bg-[var(--surface)] px-3 py-2.5 text-[13.5px] text-[var(--ink)] transition placeholder:text-[var(--ink-faint)] hover:border-[var(--accent-mid)] focus-visible:border-[var(--accent)] focus-visible:bg-[oklch(0.99_0.005_75)] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}