Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Basisbausteine im Look des UI-Mockups (docs/ISMS-Prototyp-GEFIM.html —
|
||||
* historisches Projektartefakt, der Dateiname bleibt bewusst unverändert):
|
||||
* PageHead (Crumb/Titel/Sub + Aktionen), KPI-Kapsel, C/I/A-Quadrate,
|
||||
* Tag-Chip, Owner-Chip, Status-Pille.
|
||||
*/
|
||||
|
||||
export function PageHead({
|
||||
crumb,
|
||||
title,
|
||||
sub,
|
||||
actions,
|
||||
}: {
|
||||
crumb: string;
|
||||
title: string;
|
||||
sub?: string;
|
||||
actions?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4.5 flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">{crumb}</div>
|
||||
<h1 className="text-[22px]">{title}</h1>
|
||||
{sub && <div className="mt-1 text-[13px] text-muted-foreground">{sub}</div>}
|
||||
</div>
|
||||
{actions && <div className="flex gap-2">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function KpiCard({
|
||||
label,
|
||||
value,
|
||||
trend,
|
||||
trendColor = "muted",
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
trend?: string;
|
||||
trendColor?: "muted" | "risk" | "warn" | "ok";
|
||||
}) {
|
||||
const trendColors = {
|
||||
muted: "text-muted-foreground",
|
||||
risk: "text-[var(--risk)]",
|
||||
warn: "text-[var(--warn)]",
|
||||
ok: "text-[var(--ok)]",
|
||||
};
|
||||
return (
|
||||
<div className="shadow-card rounded-xl border bg-card p-4">
|
||||
<div className="text-[12.5px] font-semibold text-muted-foreground">{label}</div>
|
||||
<div className="mt-1.5 mb-0.5 font-heading text-3xl font-bold leading-none">{value}</div>
|
||||
{trend && <div className={cn("mt-1 text-xs font-semibold", trendColors[trendColor])}>{trend}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Segmentbalken-Farben je Stufe 1–4 als Ampel: Grün → Gelb → Orange → Rot
|
||||
const CIA_BAR_COLORS: Record<number, string> = {
|
||||
1: "bg-[#2e9e6b]",
|
||||
2: "bg-[#e5b000]",
|
||||
3: "bg-[#e07d2e]",
|
||||
4: "bg-[#d64c4c]",
|
||||
};
|
||||
|
||||
/** Ein Schutzziel als 4er-Segmentbalken, gefüllt bis `level`. */
|
||||
function CiaSegment({ level, label }: { level: number; label?: string }) {
|
||||
const color = CIA_BAR_COLORS[level] ?? CIA_BAR_COLORS[1];
|
||||
return (
|
||||
<span className="inline-flex flex-col items-center gap-1">
|
||||
<span className="flex gap-[2px]">
|
||||
{[1, 2, 3, 4].map((seg) => (
|
||||
<span
|
||||
key={seg}
|
||||
className={cn(
|
||||
"h-4 w-[7px] rounded-[2px]",
|
||||
seg <= level ? color : "bg-[rgba(120,135,180,0.2)]"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
{label && (
|
||||
<small className="text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
|
||||
{label}
|
||||
</small>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schutzbedarf / Schadenshöhe als drei Segmentbalken (C/I/A). Mit `labels`
|
||||
* werden VER/INT/VFB unter den Balken gezeigt (Detailkarten); ohne für
|
||||
* kompakte Tabellenzellen.
|
||||
*/
|
||||
export function CiaBadge({
|
||||
c,
|
||||
i,
|
||||
a,
|
||||
labels = false,
|
||||
}: {
|
||||
c: number;
|
||||
i: number;
|
||||
a: number;
|
||||
labels?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5" title={`C${c} · I${i} · A${a}`}>
|
||||
<CiaSegment level={c} label={labels ? "VER" : undefined} />
|
||||
<CiaSegment level={i} label={labels ? "INT" : undefined} />
|
||||
<CiaSegment level={a} label={labels ? "VFB" : undefined} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** VER/INT/VFB-Beschriftung passend zur Balkenbreite — einmal pro Tabelle als Überschrift. */
|
||||
export function CiaLegend() {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5 text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
|
||||
{["VER", "INT", "VFB"].map((l) => (
|
||||
<span key={l} className="w-[34px] text-center">
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tag({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwnerLabel: string }) {
|
||||
if (!name) return <Pill tone="risk">{noOwnerLabel}</Pill>;
|
||||
const initials = name
|
||||
.split(/\s+/)
|
||||
.map((p) => p[0])
|
||||
.slice(0, 2)
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<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}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Status-Chips: ~16–20 % Alpha der Statusfarbe auf Dunkel (Referenz-Vorgabe)
|
||||
const PILL_TONES = {
|
||||
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({
|
||||
tone,
|
||||
children,
|
||||
}: {
|
||||
tone: keyof typeof PILL_TONES;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[11.5px] font-bold whitespace-nowrap",
|
||||
PILL_TONES[tone]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Kritikalität 1–4 als Pille in Mockup-Farben (info/warn/risk). */
|
||||
export function CriticalityPill({ level, label }: { level: number; label: string }) {
|
||||
const tone = level >= 4 ? "risk" : level === 3 ? "warn" : level === 2 ? "info" : "ok";
|
||||
return <Pill tone={tone}>{label}</Pill>;
|
||||
}
|
||||
|
||||
export function SectTitle({ title, sub }: { title: string; sub?: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="font-heading text-[15px] font-semibold">{title}</div>
|
||||
{sub && <div className="mt-0.5 text-[12.5px] text-muted-foreground">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user