Apply linen design system across app
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user