GUI ans Mockup-Design angepasst (GEFIM-Branding)

- GEFIM-Farbwelt aus dem Mockup als shadcn-Theme: Violett #5d52a3 als
  Primärfarbe, Magenta/Hellblau-Akzente, Ink-Text, #f5f6fa-Hintergrund,
  Verlaufs-Utilities (bg-grad, bg-grad-soft), Karten-Schatten
- Fonts Poppins (Überschriften/Buttons) + Open Sans (Text), self-hosted
  über next/font — kein Google-CDN-Aufruf zur Laufzeit (DSGVO)
- Sidebar mit extrahiertem GEFIM-Logo (public/gefim-logo.png), aktive
  Navigation in Violett wie im Prototyp
- Sticky Topbar mit globaler Suche (leitet vorerst auf die Asset-Suche),
  Nutzer-Info und Verlaufs-Avatar mit Initialen; Logout in die Topbar
- Primär-Buttons mit Verlaufshintergrund, Level-Badges als Pillen in
  Mockup-Farben, Login-Seite im GEFIM-Look
- Dashboard im Mockup-Stil: KPI-Karten (Assets, Prozesse, kritische
  Prozesse, Risiken-Platzhalter) und Letzte-Aktivitäten-Feed aus dem
  echten Audit-Log

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-02 13:51:11 +02:00
co-authored by Claude Fable 5
parent 392bb246cf
commit 0befcc6a2c
13 changed files with 293 additions and 163 deletions
+11 -1
View File
@@ -45,7 +45,17 @@
"welcome": "Willkommen, {name}", "welcome": "Willkommen, {name}",
"tenant": "Mandant", "tenant": "Mandant",
"roles": "Rollen", "roles": "Rollen",
"placeholder": "Die Modul-Dashboards (Risiken, Aufgaben, SoA-Erfüllungsgrad) folgen in den nächsten Iterationen." "placeholder": "Die Modul-Dashboards (Risiken, Aufgaben, SoA-Erfüllungsgrad) folgen in den nächsten Iterationen.",
"subtitle": "Willkommen zurück, {name} — Status des ISMS für {tenant}.",
"kpiAssets": "Assets im Inventar",
"kpiProcesses": "Prozesse (BIA)",
"kpiCritical": "Kritische Prozesse",
"kpiCriticalHint": "Kritikalität hoch oder sehr hoch",
"kpiRisks": "Offene Risiken",
"kpiRisksHint": "folgt mit dem Risiko-Modul",
"activity": "Letzte Aktivitäten",
"activitySub": "Audit-Log",
"activityEmpty": "Noch keine Aktivitäten."
}, },
"assets": { "assets": {
"title": "Assets & BIA", "title": "Assets & BIA",
+11 -1
View File
@@ -45,7 +45,17 @@
"welcome": "Welcome, {name}", "welcome": "Welcome, {name}",
"tenant": "Tenant", "tenant": "Tenant",
"roles": "Roles", "roles": "Roles",
"placeholder": "Module dashboards (risks, tasks, SoA coverage) follow in upcoming iterations." "placeholder": "Module dashboards (risks, tasks, SoA coverage) follow in upcoming iterations.",
"subtitle": "Welcome back, {name} — ISMS status for {tenant}.",
"kpiAssets": "Assets in inventory",
"kpiProcesses": "Processes (BIA)",
"kpiCritical": "Critical processes",
"kpiCriticalHint": "criticality high or very high",
"kpiRisks": "Open risks",
"kpiRisksHint": "coming with the risk module",
"activity": "Recent activity",
"activitySub": "Audit log",
"activityEmpty": "No activity yet."
}, },
"assets": { "assets": {
"title": "Assets & BIA", "title": "Assets & BIA",
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+71 -12
View File
@@ -1,26 +1,85 @@
import { getTranslations } from "next-intl/server"; import { getFormatter, getTranslations } from "next-intl/server";
import { requireSession } from "@/server/auth"; import { requireSession } from "@/server/auth";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { dbForTenant } from "@/server/db";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default async function DashboardPage() { export default async function DashboardPage() {
const session = await requireSession(); const session = await requireSession();
const t = await getTranslations("dashboard"); const t = await getTranslations("dashboard");
const tc = await getTranslations("common");
const format = await getFormatter();
const db = dbForTenant(session.user.tenantId);
const [assetCount, processCount, criticalCount, activities] = await Promise.all([
db.asset.count(),
db.process.count(),
db.biaEntry.count({ where: { criticality: { gte: 3 } } }),
db.auditLog.findMany({
orderBy: { createdAt: "desc" },
take: 6,
}),
]);
const actorNames = new Map<string, string>();
const actorIds = [...new Set(activities.map((a) => a.actorId).filter(Boolean))] as string[];
if (actorIds.length) {
const users = await db.user.findMany({
where: { id: { in: actorIds } },
select: { id: true, name: true },
});
users.forEach((u) => actorNames.set(u.id, u.name));
}
const kpis = [
{ label: t("kpiAssets"), value: String(assetCount), hint: null },
{ label: t("kpiProcesses"), value: String(processCount), hint: null },
{ label: t("kpiCritical"), value: String(criticalCount), hint: t("kpiCriticalHint") },
{ label: t("kpiRisks"), value: tc("none"), hint: t("kpiRisksHint") },
];
return ( return (
<main className="flex-1 p-6"> <main className="flex-1 p-6">
<h1 className="text-xl font-semibold">{t("title")}</h1> <h1 className="text-xl">{t("title")}</h1>
<Card className="mt-6 max-w-2xl"> <p className="mt-1 text-sm text-muted-foreground">
{t("subtitle", { name: session.user.name ?? "", tenant: session.user.tenantSlug })}
</p>
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{kpis.map((kpi) => (
<Card key={kpi.label}>
<CardContent>
<p className="text-[12.5px] font-semibold text-muted-foreground">{kpi.label}</p>
<p className="mt-1.5 font-heading text-3xl font-bold leading-none">{kpi.value}</p>
{kpi.hint && <p className="mt-1.5 text-xs text-muted-foreground">{kpi.hint}</p>}
</CardContent>
</Card>
))}
</div>
<Card className="mt-6 max-w-3xl">
<CardHeader> <CardHeader>
<CardTitle>{t("welcome", { name: session.user.name ?? "" })}</CardTitle> <CardTitle>{t("activity")}</CardTitle>
<CardDescription>{t("activitySub")}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<dl className="grid grid-cols-2 gap-2 text-sm"> {activities.length === 0 && (
<dt className="text-muted-foreground">{t("tenant")}</dt> <p className="text-sm text-muted-foreground">{t("activityEmpty")}</p>
<dd>{session.user.tenantSlug}</dd> )}
<dt className="text-muted-foreground">{t("roles")}</dt> <ul className="space-y-2 text-sm">
<dd>{session.user.roles.join(", ")}</dd> {activities.map((a) => (
</dl> <li key={a.id} className="flex items-baseline gap-3">
<p className="mt-6 text-sm text-muted-foreground">{t("placeholder")}</p> <span className="shrink-0 text-xs text-muted-foreground">
{format.relativeTime(a.createdAt)}
</span>
<span>
<strong className="font-semibold">
{a.actorId ? actorNames.get(a.actorId) ?? "System" : "System"}
</strong>{" "}
· {a.action} · {a.entity}
</span>
</li>
))}
</ul>
</CardContent> </CardContent>
</Card> </Card>
</main> </main>
+51 -15
View File
@@ -1,4 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -14,6 +15,7 @@ import {
FolderCheck, FolderCheck,
Truck, Truck,
LineChart, LineChart,
Search,
} from "lucide-react"; } from "lucide-react";
import { auth, signOut } from "@/server/auth"; import { auth, signOut } from "@/server/auth";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -43,6 +45,13 @@ export default async function AppLayout({
{ href: "/review", label: t("review"), icon: LineChart, enabled: false }, { href: "/review", label: t("review"), icon: LineChart, enabled: false },
]; ];
const initials = (session.user.name ?? "?")
.split(/\s+/)
.map((p) => p[0])
.slice(0, 2)
.join("")
.toUpperCase();
async function logout() { async function logout() {
"use server"; "use server";
await signOut({ redirectTo: "/login" }); await signOut({ redirectTo: "/login" });
@@ -50,42 +59,69 @@ export default async function AppLayout({
return ( return (
<div className="flex min-h-screen"> <div className="flex min-h-screen">
<aside className="flex w-60 shrink-0 flex-col border-r bg-sidebar"> <aside className="flex w-60 shrink-0 flex-col border-r border-sidebar-border bg-sidebar">
<div className="flex h-14 items-center border-b px-4"> <div className="border-b border-sidebar-border px-4 pt-5 pb-3.5">
<Link href="/dashboard" className="font-semibold tracking-tight"> <Link href="/dashboard">
{tc("appName")} <Image
src="/gefim-logo.png"
alt="GEFIM — Informationssicherheit im Mittelstand"
width={150}
height={53}
priority
/>
</Link> </Link>
</div> </div>
<nav className="flex-1 space-y-0.5 overflow-y-auto p-2"> <nav className="flex-1 space-y-0.5 overflow-y-auto px-2.5 py-3">
{nav.map((item) => {nav.map((item) =>
item.enabled ? ( item.enabled ? (
<NavLink key={item.href} href={item.href} match={item.match}> <NavLink key={item.href} href={item.href} match={item.match}>
<item.icon className="size-4" /> <item.icon className="size-[18px] opacity-85" />
{item.label} {item.label}
</NavLink> </NavLink>
) : ( ) : (
<span <span
key={item.href} key={item.href}
title={tc("comingSoon")} title={tc("comingSoon")}
className="flex cursor-not-allowed items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground/50" className="flex cursor-not-allowed items-center gap-2.5 rounded-lg px-3 py-2 text-[13.5px] font-semibold text-muted-foreground/50"
> >
<item.icon className="size-4" /> <item.icon className="size-[18px]" />
{item.label} {item.label}
</span> </span>
) )
)} )}
</nav> </nav>
<div className="border-t p-3 text-xs text-muted-foreground"> </aside>
<p className="truncate font-medium text-foreground">{session.user.name}</p>
<p className="truncate">{session.user.tenantSlug}</p> <div className="flex min-w-0 flex-1 flex-col">
<form action={logout} className="mt-2"> <header className="sticky top-0 z-10 flex items-center gap-4 border-b bg-white/90 px-6 py-2.5 backdrop-blur-md">
<Button type="submit" variant="outline" size="sm" className="w-full"> <form
method="GET"
action="/assets"
className="flex w-full max-w-105 items-center gap-2 rounded-lg border bg-muted px-3 py-2 text-muted-foreground"
>
<Search className="size-4 shrink-0" />
<input
name="q"
placeholder={tc("search")}
className="w-full border-0 bg-transparent text-[13px] text-foreground outline-none placeholder:text-muted-foreground"
/>
</form>
<div className="flex-1" />
<div className="text-right leading-tight">
<p className="text-[13px] font-semibold">{session.user.name}</p>
<p className="text-xs text-muted-foreground">{session.user.tenantSlug}</p>
</div>
<div className="bg-grad-soft grid size-9.5 shrink-0 place-items-center rounded-full font-heading text-sm font-bold text-white">
{initials}
</div>
<form action={logout}>
<Button type="submit" variant="ghost" size="sm">
{tc("logout")} {tc("logout")}
</Button> </Button>
</form> </form>
</header>
{children}
</div> </div>
</aside>
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
</div> </div>
); );
} }
+98 -73
View File
@@ -4,15 +4,12 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* Helles Basis-Theme; Dark-Mode folgt kontrolliert mit dem shadcn/ui-Theming
(kein automatisches prefers-color-scheme, damit Komponenten konsistent bleiben). */
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-sans); --font-sans: var(--font-open-sans);
--font-mono: var(--font-geist-mono); --font-mono: ui-monospace, monospace;
--font-heading: var(--font-sans); --font-heading: var(--font-poppins);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -51,77 +48,85 @@
--radius-4xl: calc(var(--radius) * 2.6); --radius-4xl: calc(var(--radius) * 2.6);
} }
body { /* GEFIM-Farbwelt aus dem Mockup (docs/ISMS-Prototyp-GEFIM.html) */
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
}
:root { :root {
--background: oklch(1 0 0); --gefim-violet: #5d52a3;
--foreground: oklch(0.145 0 0); --gefim-magenta: #812d80;
--card: oklch(1 0 0); --gefim-blue: #8dc4e0;
--card-foreground: oklch(0.145 0 0); --gefim-violet-050: #f2f0f9;
--popover: oklch(1 0 0); --gefim-violet-100: #e6e2f3;
--popover-foreground: oklch(0.145 0 0); --gefim-blue-050: #eef7fb;
--primary: oklch(0.205 0 0); --grad: linear-gradient(135deg, #5d52a3 0%, #812d80 45%, #8dc4e0 100%);
--primary-foreground: oklch(0.985 0 0); --grad-soft: linear-gradient(135deg, #5d52a3 0%, #8dc4e0 100%);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --background: #f5f6fa;
--muted: oklch(0.97 0 0); --foreground: #3b3b3a; /* ink */
--muted-foreground: oklch(0.556 0 0); --card: #ffffff;
--accent: oklch(0.97 0 0); --card-foreground: #3b3b3a;
--accent-foreground: oklch(0.205 0 0); --popover: #ffffff;
--destructive: oklch(0.577 0.245 27.325); --popover-foreground: #3b3b3a;
--border: oklch(0.922 0 0); --primary: #5d52a3;
--input: oklch(0.922 0 0); --primary-foreground: #ffffff;
--ring: oklch(0.708 0 0); --secondary: #f2f0f9; /* violet-050 */
--chart-1: oklch(0.87 0 0); --secondary-foreground: #5d52a3;
--chart-2: oklch(0.556 0 0); --muted: #f1f2f7;
--chart-3: oklch(0.439 0 0); --muted-foreground: #7a7b86;
--chart-4: oklch(0.371 0 0); --accent: #f2f0f9;
--chart-5: oklch(0.269 0 0); --accent-foreground: #5d52a3;
--radius: 0.625rem; --destructive: #d64c4c;
--sidebar: oklch(0.985 0 0); --border: #e7e8ef;
--sidebar-foreground: oklch(0.145 0 0); --input: #e7e8ef;
--sidebar-primary: oklch(0.205 0 0); --ring: #5d52a3;
--sidebar-primary-foreground: oklch(0.985 0 0); --chart-1: #5d52a3;
--sidebar-accent: oklch(0.97 0 0); --chart-2: #8dc4e0;
--sidebar-accent-foreground: oklch(0.205 0 0); --chart-3: #812d80;
--sidebar-border: oklch(0.922 0 0); --chart-4: #2e9e6b;
--sidebar-ring: oklch(0.708 0 0); --chart-5: #e0982e;
--radius: 0.7rem; /* Buttons ~10px, Karten (xl) ~16px wie im Mockup */
--sidebar: #ffffff;
--sidebar-foreground: #4b4c57;
--sidebar-primary: #5d52a3;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #e6e2f3; /* violet-100 für aktive Einträge */
--sidebar-accent-foreground: #5d52a3;
--sidebar-border: #e7e8ef;
--sidebar-ring: #5d52a3;
--shadow-card: 0 1px 3px rgba(30, 25, 60, 0.06), 0 6px 20px rgba(30, 25, 60, 0.06);
} }
/* Dark-Mode-Werte folgen kontrolliert später (kein prefers-color-scheme-Automatismus) */
.dark { .dark {
--background: oklch(0.145 0 0); --background: #1c1b22;
--foreground: oklch(0.985 0 0); --foreground: #ececf1;
--card: oklch(0.205 0 0); --card: #26242e;
--card-foreground: oklch(0.985 0 0); --card-foreground: #ececf1;
--popover: oklch(0.205 0 0); --popover: #26242e;
--popover-foreground: oklch(0.985 0 0); --popover-foreground: #ececf1;
--primary: oklch(0.922 0 0); --primary: #8b7fd4;
--primary-foreground: oklch(0.205 0 0); --primary-foreground: #1c1b22;
--secondary: oklch(0.269 0 0); --secondary: #2e2b3a;
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: #c9c3ec;
--muted: oklch(0.269 0 0); --muted: #2e2b3a;
--muted-foreground: oklch(0.708 0 0); --muted-foreground: #9a99a6;
--accent: oklch(0.269 0 0); --accent: #2e2b3a;
--accent-foreground: oklch(0.985 0 0); --accent-foreground: #c9c3ec;
--destructive: oklch(0.704 0.191 22.216); --destructive: #e06c6c;
--border: oklch(1 0 0 / 10%); --border: rgba(255, 255, 255, 0.1);
--input: oklch(1 0 0 / 15%); --input: rgba(255, 255, 255, 0.15);
--ring: oklch(0.556 0 0); --ring: #8b7fd4;
--chart-1: oklch(0.87 0 0); --sidebar: #26242e;
--chart-2: oklch(0.556 0 0); --sidebar-foreground: #ececf1;
--chart-3: oklch(0.439 0 0); --sidebar-primary: #8b7fd4;
--chart-4: oklch(0.371 0 0); --sidebar-primary-foreground: #1c1b22;
--chart-5: oklch(0.269 0 0); --sidebar-accent: #2e2b3a;
--sidebar: oklch(0.205 0 0); --sidebar-accent-foreground: #c9c3ec;
--sidebar-foreground: oklch(0.985 0 0); --sidebar-border: rgba(255, 255, 255, 0.1);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-ring: #8b7fd4;
--sidebar-primary-foreground: oklch(0.985 0 0); }
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0); body {
--sidebar-border: oklch(1 0 0 / 10%); font-family: var(--font-sans), "Open Sans", system-ui, sans-serif;
--sidebar-ring: oklch(0.556 0 0); font-size: 14px;
} }
@layer base { @layer base {
@@ -134,4 +139,24 @@ body {
html { html {
@apply font-sans; @apply font-sans;
} }
h1,
h2,
h3,
h4 {
font-family: var(--font-heading), "Poppins", sans-serif;
font-weight: 600;
}
}
@layer utilities {
/* Verlaufs-Flächen wie im Mockup (.btn.primary, Avatare, Akzente) */
.bg-grad {
background: var(--grad);
}
.bg-grad-soft {
background: var(--grad-soft);
}
.shadow-card {
box-shadow: var(--shadow-card);
}
} }
+10 -7
View File
@@ -1,17 +1,20 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Open_Sans, Poppins } from "next/font/google";
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ // Self-hosted über next/font (kein Google-CDN-Aufruf zur Laufzeit, DSGVO)
variable: "--font-geist-sans", const openSans = Open_Sans({
variable: "--font-open-sans",
subsets: ["latin"], subsets: ["latin"],
weight: ["300", "400", "600", "700"],
}); });
const geistMono = Geist_Mono({ const poppins = Poppins({
variable: "--font-geist-mono", variable: "--font-poppins",
subsets: ["latin"], subsets: ["latin"],
weight: ["300", "500", "600", "700"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -30,9 +33,9 @@ export default async function RootLayout({
return ( return (
<html <html
lang={locale} lang={locale}
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${openSans.variable} ${poppins.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col bg-slate-50 text-slate-900"> <body className="min-h-full flex flex-col bg-background text-foreground">
<NextIntlClientProvider>{children}</NextIntlClientProvider> <NextIntlClientProvider>{children}</NextIntlClientProvider>
</body> </body>
</html> </html>
+24 -38
View File
@@ -1,7 +1,11 @@
import Image from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AuthError } from "next-auth"; import { AuthError } from "next-auth";
import { auth, signIn } from "@/server/auth"; import { auth, signIn } from "@/server/auth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default async function LoginPage({ export default async function LoginPage({
searchParams, searchParams,
@@ -9,7 +13,6 @@ export default async function LoginPage({
searchParams: Promise<{ error?: string; callbackUrl?: string }>; searchParams: Promise<{ error?: string; callbackUrl?: string }>;
}) { }) {
const t = await getTranslations("login"); const t = await getTranslations("login");
const tc = await getTranslations("common");
const { error, callbackUrl } = await searchParams; const { error, callbackUrl } = await searchParams;
// Already signed in → straight to the app // Already signed in → straight to the app
@@ -36,14 +39,20 @@ export default async function LoginPage({
return ( return (
<main className="flex flex-1 items-center justify-center p-6"> <main className="flex flex-1 items-center justify-center p-6">
<div className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-8 shadow-sm"> <div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<h1 className="text-xl font-semibold">{tc("appName")}</h1> <Image
<h2 className="mt-1 text-sm text-slate-500">{t("subtitle")}</h2> src="/gefim-logo.png"
alt="GEFIM — Informationssicherheit im Mittelstand"
width={170}
height={60}
priority
/>
<h2 className="mt-4 text-sm font-normal text-muted-foreground">{t("subtitle")}</h2>
{error && ( {error && (
<p <p
role="alert" role="alert"
className="mt-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700" className="mt-4 rounded-lg bg-[#fbe4e4] px-3 py-2 text-sm text-[#d64c4c]"
> >
{t("error")} {t("error")}
</p> </p>
@@ -52,51 +61,28 @@ export default async function LoginPage({
<form action={login} className="mt-6 space-y-4"> <form action={login} className="mt-6 space-y-4">
<input type="hidden" name="callbackUrl" value={callbackUrl ?? ""} /> <input type="hidden" name="callbackUrl" value={callbackUrl ?? ""} />
<div> <div>
<label htmlFor="email" className="block text-sm font-medium"> <Label htmlFor="email">{t("email")}</Label>
{t("email")} <Input id="email" name="email" type="email" required autoComplete="email" className="mt-1" />
</label>
<input
id="email"
name="email"
type="email"
required
autoComplete="email"
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm font-medium"> <Label htmlFor="password">{t("password")}</Label>
{t("password")} <Input
</label>
<input
id="password" id="password"
name="password" name="password"
type="password" type="password"
required required
autoComplete="current-password" autoComplete="current-password"
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" className="mt-1"
/> />
</div> </div>
<div> <div>
<label htmlFor="tenant" className="block text-sm font-medium"> <Label htmlFor="tenant">{t("tenant")}</Label>
{t("tenant")} <Input id="tenant" name="tenant" autoComplete="organization" className="mt-1" />
</label> <p className="mt-1 text-xs text-muted-foreground">{t("tenantHint")}</p>
<input
id="tenant"
name="tenant"
type="text"
autoComplete="organization"
title={t("tenantHint")}
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<p className="mt-1 text-xs text-slate-400">{t("tenantHint")}</p>
</div> </div>
<button <Button type="submit" className="w-full">
type="submit"
className="w-full rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{t("submit")} {t("submit")}
</button> </Button>
</form> </form>
</div> </div>
</main> </main>
+1 -1
View File
@@ -12,7 +12,7 @@ export function LevelBadge({
prefix?: string; prefix?: string;
}) { }) {
return ( return (
<Badge variant="outline" className={levelColor(level)}> <Badge variant="outline" className={`font-bold ${levelColor(level)}`}>
{prefix ? `${prefix}${level}` : label ?? level} {prefix ? `${prefix}${level}` : label ?? level}
</Badge> </Badge>
); );
+3 -3
View File
@@ -22,10 +22,10 @@ export function NavLink({
<Link <Link
href={href} href={href}
className={cn( className={cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", "flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13.5px] font-semibold transition-colors",
active active
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" ? "bg-sidebar-accent text-sidebar-accent-foreground [&_svg]:opacity-100"
: "text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground" : "text-sidebar-foreground hover:bg-secondary hover:text-foreground"
)} )}
> >
{children} {children}
+3 -2
View File
@@ -4,11 +4,12 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding font-heading text-sm font-semibold whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80", // Primär-Buttons mit GEFIM-Verlauf wie im Mockup
default: "bg-grad-soft text-white hover:opacity-90",
outline: outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary: secondary:
+1 -1
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card" data-slot="card"
data-size={size} data-size={size}
className={cn( className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground shadow-card ring-1 ring-border [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className className
)} )}
{...props} {...props}
+5 -5
View File
@@ -1,13 +1,13 @@
/** Farbklassen für Stufen 14 (Schutzbedarf, Schadenshöhe, Kritikalität). */ /** Farbklassen für Stufen 14 (Schutzbedarf, Schadenshöhe, Kritikalität) — Pillen-Farben aus dem Mockup. */
export function levelColor(level: number): string { export function levelColor(level: number): string {
switch (level) { switch (level) {
case 2: case 2:
return "bg-yellow-100 text-yellow-800 border-yellow-200"; return "bg-[#fbf1de] text-[#a9760f] border-transparent";
case 3: case 3:
return "bg-orange-100 text-orange-800 border-orange-200"; return "bg-[#fdeadb] text-[#c2681a] border-transparent";
case 4: case 4:
return "bg-red-100 text-red-800 border-red-200"; return "bg-[#fbe4e4] text-[#d64c4c] border-transparent";
default: default:
return "bg-emerald-100 text-emerald-800 border-emerald-200"; return "bg-[#e7f6ee] text-[#2e9e6b] border-transparent";
} }
} }