Iteration Teil A: Dark-Theme (zentrale Token-Umstellung)

Referenz: docs/ISMS-Abhaengigkeitskarte-GEFIM.html

- Zentrale Theme-Definition in globals.css auf das dunkle GEFIM-Muster:
  Flächen (--bg-0/1, --panel, --elevated), Text (--txt/--muted),
  Marke (aufgehelltes Violett #7d6fd6), Status (--ok/warn/risk/info),
  Verläufe (--grad-primary/--grad-critical), Glow-Ambiente auf dem Body,
  Fokus-Ring, Punktraster-Utility
- Alle shadcn-Tokens (background/card/popover/primary/muted/border/
  sidebar …) auf Dunkelwerte gemappt; neue semantische Flächen-Tokens
  (panel/elevated/surface-soft/band/band-text/band-brd) ergänzt
- Hardcodierte Hex projektweit auf Tokens umgestellt: Panel-/Band-
  Flächen, Status-Pillen (Alpha-Tints), Tags, Trend-/Prioritätsfarben,
  Kanban, Login-Fehler, Topbar, Filter-Tabs
- GEFIM-Logo als weißes Negativ auf Dunkel (Sidebar + Login)
- Ampel-Segmentbalken/Score-Skala bleiben als semantische Statusfarben

Dark ist Default; Light-Mode später über dieselben Token-Namen möglich.
Verifiziert: Dashboard, Risiko-Detail, Asset-Bearbeiten dunkel & lesbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-03 08:55:35 +02:00
co-authored by Claude Opus 4.8
parent 1b13af46db
commit 7afd7f6acf
14 changed files with 379 additions and 107 deletions
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -70,6 +70,8 @@ export default async function AppLayout({
width={150}
height={53}
priority
// Negativ-/Weiß-Darstellung auf dunklem Grund
className="brightness-0 invert"
/>
</Link>
</div>
@@ -95,7 +97,7 @@ export default async function AppLayout({
</aside>
<div className="flex min-w-0 flex-1 flex-col">
<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">
<header className="sticky top-0 z-10 flex items-center gap-4 border-b bg-[var(--panel)] px-6 py-2.5 backdrop-blur-md">
<form
method="GET"
action="/assets"
+3 -3
View File
@@ -23,9 +23,9 @@ const MEASURE_INCLUDE = {
} as const;
const PRIORITY_CLASS = {
LOW: "bg-[#eef0f4] text-[#7a7b86]",
MEDIUM: "bg-[#e7f1fa] text-[#3a86c8]",
HIGH: "bg-[#fbe7e7] text-[#d64c4c]",
LOW: "bg-[rgba(139,147,173,0.16)] text-muted-foreground",
MEDIUM: "bg-[rgba(90,169,230,0.16)] text-[var(--info)]",
HIGH: "bg-[rgba(255,107,107,0.16)] text-[var(--risk)]",
} as const;
export default async function MeasuresPage({
+1 -1
View File
@@ -178,7 +178,7 @@ export default async function RisksPage({
key={r.id}
href={`/risks?detail=${r.id}`}
title={r.title}
className="rounded-md bg-white/85 px-1.5 py-0.5 font-heading text-[10.5px] font-bold text-foreground shadow-sm hover:bg-white"
className="rounded-md bg-[var(--elevated)] px-1.5 py-0.5 font-heading text-[10.5px] font-bold text-foreground shadow-sm hover:bg-[var(--elevated)]"
>
{riskRef(r.refNo)}
</Link>
+96 -72
View File
@@ -39,6 +39,18 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
/* Semantische Zusatzflächen (dunkles GEFIM-Theme) */
--color-panel: var(--panel);
--color-panel-brd: var(--panel-brd);
--color-elevated: var(--elevated);
--color-surface-soft: var(--surface-soft);
--color-band: var(--band);
--color-band-text: var(--band-text);
--color-band-brd: var(--band-brd);
--color-ok: var(--ok);
--color-warn: var(--warn);
--color-risk: var(--risk);
--color-info: var(--info);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
@@ -48,85 +60,91 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
/* GEFIM-Farbwelt aus dem Mockup (docs/ISMS-Prototyp-GEFIM.html) */
/*
* Dunkles GEFIM-Theme (Default) — zentrale Token-Definition.
* Referenz: docs/ISMS-Abhaengigkeitskarte-GEFIM.html.
* Ein späterer Light-Mode kann über dieselben Token-Namen unter .light ergänzt werden.
*/
:root {
--gefim-violet: #5d52a3;
--gefim-magenta: #812d80;
/* Marke / Verläufe */
--gefim-violet: #7d6fd6;
--gefim-violet-deep: #5d52a3;
--gefim-blue: #8dc4e0;
--gefim-violet-050: #f2f0f9;
--gefim-violet-100: #e6e2f3;
--gefim-blue-050: #eef7fb;
--grad: linear-gradient(135deg, #5d52a3 0%, #812d80 45%, #8dc4e0 100%);
--grad-soft: linear-gradient(135deg, #5d52a3 0%, #8dc4e0 100%);
--gefim-magenta: #b45bb0;
--grad: var(--grad-primary);
--grad-primary: linear-gradient(135deg, #5d52a3 0%, #7d6fd6 100%);
--grad-soft: linear-gradient(135deg, #5d52a3 0%, #7d6fd6 100%);
--grad-critical: linear-gradient(90deg, #ff6b6b 0%, #b45bb0 100%);
--background: #f5f6fa;
--foreground: #3b3b3a; /* ink */
--card: #ffffff;
--card-foreground: #3b3b3a;
--popover: #ffffff;
--popover-foreground: #3b3b3a;
--primary: #5d52a3;
/* Status */
--ok: #39c07f;
--warn: #f0ad4e;
--risk: #ff6b6b;
--info: #5aa9e6;
/* Flächen */
--background: #0e1220;
--foreground: #e8ecf7;
--card: #141a2e;
--card-foreground: #e8ecf7;
--popover: #1a2138;
--popover-foreground: #e8ecf7;
--primary: #7d6fd6;
--primary-foreground: #ffffff;
--secondary: #f2f0f9; /* violet-050 */
--secondary-foreground: #5d52a3;
--muted: #f1f2f7;
--muted-foreground: #7a7b86;
--accent: #f2f0f9;
--accent-foreground: #5d52a3;
--destructive: #d64c4c;
--border: #e7e8ef;
--input: #e7e8ef;
--ring: #5d52a3;
--chart-1: #5d52a3;
--secondary: #1a2138;
--secondary-foreground: #cfc9ee;
--muted: #1a2138;
--muted-foreground: #8b93ad;
--accent: #1e2640;
--accent-foreground: #cfc9ee;
--destructive: #ff6b6b;
--border: rgba(120, 135, 180, 0.18);
--input: rgba(120, 135, 180, 0.22);
--ring: #7d6fd6;
--chart-1: #7d6fd6;
--chart-2: #8dc4e0;
--chart-3: #812d80;
--chart-4: #2e9e6b;
--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);
}
--chart-3: #b45bb0;
--chart-4: #39c07f;
--chart-5: #f0ad4e;
--radius: 0.7rem;
/* Dark-Mode-Werte folgen kontrolliert später (kein prefers-color-scheme-Automatismus) */
.dark {
--background: #1c1b22;
--foreground: #ececf1;
--card: #26242e;
--card-foreground: #ececf1;
--popover: #26242e;
--popover-foreground: #ececf1;
--primary: #8b7fd4;
--primary-foreground: #1c1b22;
--secondary: #2e2b3a;
--secondary-foreground: #c9c3ec;
--muted: #2e2b3a;
--muted-foreground: #9a99a6;
--accent: #2e2b3a;
--accent-foreground: #c9c3ec;
--destructive: #e06c6c;
--border: rgba(255, 255, 255, 0.1);
--input: rgba(255, 255, 255, 0.15);
--ring: #8b7fd4;
--sidebar: #26242e;
--sidebar-foreground: #ececf1;
--sidebar-primary: #8b7fd4;
--sidebar-primary-foreground: #1c1b22;
--sidebar-accent: #2e2b3a;
--sidebar-accent-foreground: #c9c3ec;
--sidebar-border: rgba(255, 255, 255, 0.1);
--sidebar-ring: #8b7fd4;
/* Panels / Glas / erhöhte Flächen */
--panel: rgba(30, 38, 64, 0.72);
--panel-brd: rgba(120, 135, 180, 0.18);
--elevated: #1a2138;
/* weiche Akzentkarte (früher #faf9fd) */
--surface-soft: rgba(125, 111, 214, 0.08);
/* Info-Band (früher #f2f0f9 / #e6e2f3 / #5a4e86) */
--band: rgba(125, 111, 214, 0.1);
--band-text: #c3bdec;
--band-brd: rgba(125, 111, 214, 0.22);
/* Sidebar / Topbar */
--sidebar: #141a2e;
--sidebar-foreground: #b6bdd4;
--sidebar-primary: #7d6fd6;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: rgba(125, 111, 214, 0.18);
--sidebar-accent-foreground: #cfc9ee;
--sidebar-border: rgba(120, 135, 180, 0.18);
--sidebar-ring: #7d6fd6;
--focus-ring: 0 0 0 3px rgba(125, 111, 214, 0.35);
--radius-card: 16px;
--radius-ctrl: 10px;
--dot-grid: radial-gradient(rgba(140, 155, 200, 0.1) 1px, transparent 1px);
--glow:
radial-gradient(1200px 700px at 20% -10%, #1c2340 0%, transparent 60%),
radial-gradient(900px 600px at 100% 110%, #1a2138 0%, transparent 55%);
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 28px rgba(0, 0, 0, 0.28);
}
body {
font-family: var(--font-sans), "Open Sans", system-ui, sans-serif;
font-size: 14px;
background-color: var(--background);
background-image: var(--glow);
background-attachment: fixed;
}
@layer base {
@@ -134,7 +152,7 @@ body {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply text-foreground;
}
html {
@apply font-sans;
@@ -149,14 +167,20 @@ body {
}
@layer utilities {
/* Verlaufs-Flächen wie im Mockup (.btn.primary, Avatare, Akzente) */
.bg-grad {
background: var(--grad);
background: var(--grad-primary);
}
.bg-grad-soft {
background: var(--grad-soft);
background: var(--grad-primary);
}
.bg-grad-critical {
background: var(--grad-critical);
}
.shadow-card {
box-shadow: var(--shadow-card);
}
.dot-grid {
background-image: var(--dot-grid);
background-size: 26px 26px;
}
}
+2 -1
View File
@@ -46,13 +46,14 @@ export default async function LoginPage({
width={170}
height={60}
priority
className="brightness-0 invert"
/>
<h2 className="mt-4 text-sm font-normal text-muted-foreground">{t("subtitle")}</h2>
{error && (
<p
role="alert"
className="mt-4 rounded-lg bg-[#fbe4e4] px-3 py-2 text-sm text-[#d64c4c]"
className="mt-4 rounded-lg bg-[rgba(255,107,107,0.16)] px-3 py-2 text-sm text-[var(--risk)]"
>
{t("error")}
</p>
+2 -2
View File
@@ -95,7 +95,7 @@ export async function AssetDetailModal({
<Pill tone="violet">{t("masterPill")}</Pill>
<span className="text-[12.5px] text-muted-foreground">{t("masterNote")}</span>
</div>
<div className="rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4">
<div className="rounded-xl border border-l-[3px] border-l-[var(--primary)] bg-[var(--surface-soft)] p-4">
<div className="flex items-center justify-between gap-2">
<b>{asset.name}</b>
<CiaBadge c={asset.confidentiality} i={asset.integrity} a={asset.availability} labels />
@@ -193,7 +193,7 @@ export async function AssetDetailModal({
</div>
{/* Zugeordnete Risiken — Band wie die BIA-Kennzahlen im Prozess-Popup */}
<div className="mx-5 mb-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4 text-[12.5px] text-[#5a4e86]">
<div className="mx-5 mb-5 rounded-xl border border-[var(--band-brd)] bg-[var(--band)] p-4 text-[12.5px] text-[var(--band-text)]">
<div className="flex items-center justify-between gap-2">
<b>{t("linkedRisks")}</b>
{canWrite && (
+1 -1
View File
@@ -16,7 +16,7 @@ export function FilterTabs({
className={cn(
"rounded-full border px-3.5 py-[7px] font-heading text-[12.5px] font-semibold transition-colors",
tab.active
? "border-transparent bg-[#e6e2f3] text-[#5d52a3]"
? "border-transparent bg-[var(--sidebar-accent)] text-[var(--primary)]"
: "border-border bg-card text-muted-foreground hover:text-foreground"
)}
>
+4 -4
View File
@@ -84,12 +84,12 @@ function Column({
ref={setNodeRef}
className={cn(
"rounded-xl border bg-muted/60 p-2.5 transition-colors",
isOver && "border-[#5d52a3] bg-[#f2f0f9]"
isOver && "border-[var(--primary)] bg-[var(--band)]"
)}
>
<div className="mb-2 flex items-center justify-between px-1">
<span className="font-heading text-[12.5px] font-semibold">{column.label}</span>
<span className="rounded-full bg-white px-2 py-0.5 text-[11px] font-bold text-muted-foreground">
<span className="rounded-full bg-[var(--elevated)] px-2 py-0.5 text-[11px] font-bold text-muted-foreground">
{column.cards.length}
</span>
</div>
@@ -149,13 +149,13 @@ function Card({
</div>
<p className="mt-1 font-semibold leading-snug">{card.title}</p>
<div className="mt-2 flex items-center justify-between text-[11.5px] text-muted-foreground">
<span className={cn(card.overdue && "font-bold text-[#d64c4c]")}>
<span className={cn(card.overdue && "font-bold text-[var(--risk)]")}>
{card.dueLabel ?? "—"}
</span>
<span className="flex items-center gap-1.5">
{card.riskCount > 0 && <span title="Verknüpfte Risiken"> {card.riskCount}</span>}
{card.ownerInitials && (
<span className="grid size-6 place-items-center rounded-full bg-[#e6e2f3] text-[10px] font-bold text-[#5d52a3]">
<span className="grid size-6 place-items-center rounded-full bg-[var(--sidebar-accent)] text-[10px] font-bold text-[var(--primary)]">
{card.ownerInitials}
</span>
)}
+1 -1
View File
@@ -70,7 +70,7 @@ export async function MeasureDetailModal({
}
>
<div className="grid gap-5 p-5 md:grid-cols-2">
<div className="rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4">
<div className="rounded-xl border border-l-[3px] border-l-[var(--primary)] bg-[var(--surface-soft)] p-4">
<b>{measure.title}</b>
{measure.description && (
<p className="mt-1.5 text-[12.5px] leading-relaxed">{measure.description}</p>
+14 -13
View File
@@ -42,9 +42,9 @@ export function KpiCard({
}) {
const trendColors = {
muted: "text-muted-foreground",
risk: "text-[#d64c4c]",
warn: "text-[#e0982e]",
ok: "text-[#2e9e6b]",
risk: "text-[var(--risk)]",
warn: "text-[var(--warn)]",
ok: "text-[var(--ok)]",
};
return (
<div className="shadow-card rounded-xl border bg-card p-4">
@@ -74,7 +74,7 @@ function CiaSegment({ level, label }: { level: number; label?: string }) {
key={seg}
className={cn(
"h-4 w-[7px] rounded-[2px]",
seg <= level ? color : "bg-[#e3e5ee]"
seg <= level ? color : "bg-[rgba(120,135,180,0.2)]"
)}
/>
))}
@@ -128,7 +128,7 @@ export function CiaLegend() {
export function Tag({ children }: { children: React.ReactNode }) {
return (
<span className="inline-block rounded-md bg-[#eef0f4] px-2 py-0.5 text-[11px] font-bold text-[#555]">
<span className="inline-block rounded-md bg-[rgba(139,147,173,0.14)] px-2 py-0.5 text-[11px] font-bold text-[#b7bdd0]">
{children}
</span>
);
@@ -144,7 +144,7 @@ export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwne
.toUpperCase();
return (
<span className="inline-flex items-center gap-2">
<span className="grid size-6.5 place-items-center rounded-full bg-[#e6e2f3] font-heading text-[11px] font-bold text-[#5d52a3]">
<span className="grid size-6.5 place-items-center rounded-full bg-[var(--sidebar-accent)] font-heading text-[11px] font-bold text-[var(--primary)]">
{initials}
</span>
{name}
@@ -152,14 +152,15 @@ export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwne
);
}
// Status-Chips: ~1620 % Alpha der Statusfarbe auf Dunkel (Referenz-Vorgabe)
const PILL_TONES = {
ok: "bg-[#e7f6ee] text-[#2e9e6b]",
warn: "bg-[#fbf1de] text-[#a9760f]",
orange: "bg-[#fbdcc0] text-[#c2681a]",
risk: "bg-[#fbe7e7] text-[#d64c4c]",
info: "bg-[#e7f1fa] text-[#3a86c8]",
mut: "bg-[#eef0f4] text-muted-foreground",
violet: "bg-[#e6e2f3] text-[#5d52a3]",
ok: "bg-[rgba(57,192,127,0.16)] text-[var(--ok)]",
warn: "bg-[rgba(240,173,78,0.16)] text-[var(--warn)]",
orange: "bg-[rgba(240,140,60,0.16)] text-[#f0a35a]",
risk: "bg-[rgba(255,107,107,0.16)] text-[var(--risk)]",
info: "bg-[rgba(90,169,230,0.16)] text-[var(--info)]",
mut: "bg-[rgba(139,147,173,0.16)] text-muted-foreground",
violet: "bg-[rgba(125,111,214,0.2)] text-[#c3bdec]",
} as const;
export function Pill({
+3 -3
View File
@@ -132,7 +132,7 @@ export async function ProcessDetailModal({
{primary.map((pa) => (
<div
key={pa.id}
className="mb-2 rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4"
className="mb-2 rounded-xl border border-l-[3px] border-l-[var(--primary)] bg-[var(--surface-soft)] p-4"
>
<div className="flex items-center justify-between gap-2">
<b>{pa.asset.name}</b>
@@ -188,7 +188,7 @@ export async function ProcessDetailModal({
</div>
</div>
<div className="mx-5 mb-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4 text-[12.5px] text-[#5a4e86]">
<div className="mx-5 mb-5 rounded-xl border border-[var(--band-brd)] bg-[var(--band)] p-4 text-[12.5px] text-[var(--band-text)]">
<div className="grid gap-2 sm:grid-cols-4">
<div>
<b>RTO</b>
@@ -214,7 +214,7 @@ export async function ProcessDetailModal({
</div>
</div>
{process.bia?.notes && (
<p className="mt-3 border-t border-[#e6e2f3] pt-2.5">{process.bia.notes}</p>
<p className="mt-3 border-t border-[var(--band-brd)] pt-2.5">{process.bia.notes}</p>
)}
</div>
+4 -4
View File
@@ -89,10 +89,10 @@ async function RatingCard({
const format = await getFormatter();
const num = (v: number) => format.number(v, { maximumFractionDigits: 2 });
const border = accent === "gross" ? "border-l-[#d64c4c]" : "border-l-[#e0982e]";
const border = accent === "gross" ? "border-l-[var(--risk)]" : "border-l-[var(--warn)]";
return (
<div className={`rounded-xl border border-l-[3px] bg-white p-4 ${border}`}>
<div className={`rounded-xl border border-l-[3px] bg-[var(--elevated)] p-4 ${border}`}>
<div className="text-[11px] font-bold tracking-wide text-muted-foreground uppercase">
{title}
</div>
@@ -202,7 +202,7 @@ export async function RiskDetailModal({
>
<div className="grid gap-5 p-5 md:grid-cols-2">
<div>
<div className="rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4">
<div className="rounded-xl border border-l-[3px] border-l-[var(--primary)] bg-[var(--surface-soft)] p-4">
<b>{risk.title}</b>
{risk.description && (
<p className="mt-1.5 text-[12.5px] leading-relaxed">{risk.description}</p>
@@ -276,7 +276,7 @@ export async function RiskDetailModal({
</div>
{/* Brutto → Rest-Risiko: Farbskala oben, darunter zwei Karten mit Pfeil */}
<div className="mx-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4">
<div className="mx-5 rounded-xl border border-[var(--band-brd)] bg-[var(--band)] p-4">
<RiskScoreScale gross={risk.score} residual={risk.residualScore} />
<div className="mt-4 grid items-center gap-3 sm:grid-cols-[1fr_auto_1fr]">
<RatingCard
+1 -1
View File
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-[#faf9fd] has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
"border-b transition-colors hover:bg-[var(--surface-soft)] has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}