revert(redesign): Design-Tokens + Ebene-2 "Cyber trifft Safety" zurueckziehen
Das Frontend-Redesign wird vorerst pausiert (Fokus MVP). Der komplette
Stand ist im Git-Tag redesign-archive-20260619 (Commit 42d4b4d9)
archiviert und jederzeit fortsetzbar; die Handoff-Docs bleiben unter
design/redesign/ erhalten. Diese Aenderung zieht nur den aktiven Code
zurueck (Tokens, Chips, CyberMeetsSafety, design-system-Seite, Font-Setup) —
die UI kehrt zum vorherigen Stand zurueck.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,8 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter, Public_Sans, Source_Serif_4, IBM_Plex_Mono } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
// Redesign fonts (design/redesign) — exposed as CSS variables; the new
|
|
||||||
// design-language components opt in via Tailwind font-publicSans/-sourceSerif/-plexMono.
|
|
||||||
const publicSans = Public_Sans({ subsets: ['latin'], weight: ['400', '500', '600', '700', '800'], variable: '--font-public-sans' })
|
|
||||||
const sourceSerif = Source_Serif_4({ subsets: ['latin'], weight: ['400', '500', '600'], style: ['normal', 'italic'], variable: '--font-source-serif' })
|
|
||||||
const plexMono = IBM_Plex_Mono({ subsets: ['latin'], weight: ['400', '500', '600'], variable: '--font-plex-mono' })
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'BreakPilot Admin Compliance',
|
title: 'BreakPilot Admin Compliance',
|
||||||
@@ -20,7 +15,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="de" className={`${publicSans.variable} ${sourceSerif.variable} ${plexMono.variable}`}>
|
<html lang="de">
|
||||||
<body className={inter.className}>{children}</body>
|
<body className={inter.className}>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// Design-system showcase (Schritt A verification). Renders the redesign tokens +
|
|
||||||
// chips so the design language can be reviewed in isolation. Internal reference
|
|
||||||
// page (not in the customer sidebar).
|
|
||||||
|
|
||||||
import { GeltungChip, SeverityChip, DomainTag, MonoId } from '@/components/redesign/Chips'
|
|
||||||
import { COLORS, DOMAIN } from '@/components/redesign/tokens'
|
|
||||||
|
|
||||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<section style={{ marginTop: 38 }}>
|
|
||||||
<h2 className="font-publicSans" style={{ fontSize: 19, fontWeight: 800, letterSpacing: '-0.01em', color: COLORS.ink }}>{title}</h2>
|
|
||||||
<div style={{ marginTop: 14 }}>{children}</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Swatch({ name, hex }: { name: string; hex: string }) {
|
|
||||||
return (
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 12 }}>
|
|
||||||
<div style={{ height: 44, borderRadius: 10, background: hex, border: `1px solid ${COLORS.border}` }} />
|
|
||||||
<div style={{ marginTop: 6, color: COLORS.ink, fontWeight: 600 }}>{name}</div>
|
|
||||||
<div className="font-plexMono" style={{ color: COLORS.faint, fontSize: 11 }}>{hex}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DesignSystemPage() {
|
|
||||||
return (
|
|
||||||
<div className="font-publicSans" style={{ background: COLORS.page, minHeight: '100%', margin: -24, padding: '30px 24px 90px' }}>
|
|
||||||
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
|
|
||||||
<h1 style={{ fontSize: 30, fontWeight: 800, letterSpacing: '-0.02em', color: COLORS.ink, lineHeight: 1.1 }}>
|
|
||||||
Design-Sprache
|
|
||||||
</h1>
|
|
||||||
<p style={{ marginTop: 8, color: COLORS.muted, fontSize: 14, maxWidth: 640 }}>
|
|
||||||
Schritt A — Tokens & Bausteine des Redesigns (Geltung, Severity, Domänen, Typografie).
|
|
||||||
Referenz: <span className="font-plexMono" style={{ fontSize: 12 }}>design/redesign/HANDOFF_README.md</span>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Section title="Geltung — Pflicht / Empfehlung / Kann">
|
|
||||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
||||||
<GeltungChip value="pflicht" />
|
|
||||||
<GeltungChip value="empfehlung" />
|
|
||||||
<GeltungChip value="kann" />
|
|
||||||
<span style={{ color: COLORS.faint, fontSize: 12 }}>+ Klartext-Maßnahme</span>
|
|
||||||
<MonoId>M542</MonoId>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Severity — Dringlichkeit (getönt, kein Neon)">
|
|
||||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
||||||
<SeverityChip value="kritisch" />
|
|
||||||
<SeverityChip value="hoch" />
|
|
||||||
<SeverityChip value="mittel" />
|
|
||||||
<SeverityChip value="niedrig" />
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Domänen — Safety / Cyber / Schnittstelle">
|
|
||||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 14 }}>
|
|
||||||
<DomainTag value="safety" />
|
|
||||||
<DomainTag value="cyber" />
|
|
||||||
<DomainTag value="bridge" />
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12 }}>
|
|
||||||
{(['safety', 'cyber', 'bridge'] as const).map((k) => (
|
|
||||||
<div key={k} style={{ background: DOMAIN[k].tint, border: `1px solid ${DOMAIN[k].border}`, borderLeft: `4px solid ${DOMAIN[k].accent}`, borderRadius: 12, padding: 14 }}>
|
|
||||||
<div style={{ fontWeight: 700, color: DOMAIN[k].accent, fontSize: 13 }}>{DOMAIN[k].label}</div>
|
|
||||||
<div className="font-plexMono" style={{ color: COLORS.faint, fontSize: 11, marginTop: 4 }}>{DOMAIN[k].accent}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Typografie">
|
|
||||||
<div style={{ display: 'grid', gap: 10 }}>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 18, fontWeight: 800, color: COLORS.ink }}>Public Sans — UI & Überschriften (800)</div>
|
|
||||||
<div className="font-sourceSerif" style={{ fontSize: 15, fontStyle: 'italic', color: COLORS.muted }}>Source Serif 4 — Normzitate / rechtliche Texte (kursiv)</div>
|
|
||||||
<div className="font-plexMono" style={{ fontSize: 13, color: COLORS.muted }}>IBM Plex Mono — interne IDs · CRA-AI-8 · R = S × (F + W + P)</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Neutrale & Marke">
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 12 }}>
|
|
||||||
<Swatch name="bg/page" hex={COLORS.page} />
|
|
||||||
<Swatch name="surface" hex={COLORS.surface} />
|
|
||||||
<Swatch name="border" hex={COLORS.border} />
|
|
||||||
<Swatch name="ink" hex={COLORS.ink} />
|
|
||||||
<Swatch name="primary" hex={COLORS.brand} />
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Leitprinzipien">
|
|
||||||
<div style={{ background: COLORS.panel, borderRadius: 14, padding: 22 }}>
|
|
||||||
<ol style={{ color: COLORS.panelText2, fontSize: 13, lineHeight: 1.7, paddingLeft: 0, listStyle: 'none' }}>
|
|
||||||
{[
|
|
||||||
'3 Ebenen pro Screen: Überblick → Cyber×Safety → Technik (eingeklappt).',
|
|
||||||
'Klartext führt. Interne IDs nur in Monospace nachgestellt.',
|
|
||||||
'Co-Pilot statt Roboter-Anwalt — keine Panik-Rot-Blöcke.',
|
|
||||||
'Pflicht / Empfehlung / Kann immer visuell getrennt.',
|
|
||||||
].map((t, i) => (
|
|
||||||
<li key={i} style={{ display: 'flex', gap: 10, marginBottom: 6 }}>
|
|
||||||
<span className="font-plexMono" style={{ color: COLORS.panelAccent, fontWeight: 600 }}>{i + 1}</span>
|
|
||||||
<span style={{ color: COLORS.panelText }}>{t}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// Ebene 2 — "Cyber trifft Safety" (Redesign-Herzstück / USP).
|
|
||||||
// Macht sichtbar, wo ein Cyber-Befund eine bereits mechanisch gemilderte
|
|
||||||
// CE-Gefährdung wieder aufreißt. Bindet an die ECHTEN Bridge-Daten (cross_links)
|
|
||||||
// + findings + open_measures aus useCRA. Design nach design/redesign/HANDOFF_README.md.
|
|
||||||
// Additiv: ersetzt den bestehenden CRACyberView NICHT.
|
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { CRADemo, CrossLink, CRAFinding, Measure } from '../_hooks/useCRADemo'
|
|
||||||
import { GeltungChip, MonoId } from '@/components/redesign/Chips'
|
|
||||||
import { COLORS, DOMAIN, Geltung } from '@/components/redesign/tokens'
|
|
||||||
|
|
||||||
// Maßnahme → Geltung: technische Schutzmaßnahmen = Pflicht; Info/Hardening-Guides
|
|
||||||
// = Empfehlung. Heuristik (kein Geltung-Feld in den Daten); nie still „Kann".
|
|
||||||
function measureGeltung(name: string, id: string): Geltung {
|
|
||||||
const hay = `${id} ${name}`.toLowerCase()
|
|
||||||
return /info|guide|hardening|dokumentation|beilegen|hinweis|schulung/.test(hay) ? 'empfehlung' : 'pflicht'
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChainNode({ tone, marker, label, text }: {
|
|
||||||
tone: 'ce' | 'cyber' | 'residual'; marker: string; label: string; text: string
|
|
||||||
}) {
|
|
||||||
const s = tone === 'ce'
|
|
||||||
? { bg: DOMAIN.safety.tint, border: DOMAIN.safety.border, accent: DOMAIN.safety.accent }
|
|
||||||
: tone === 'cyber'
|
|
||||||
? { bg: DOMAIN.cyber.tint, border: DOMAIN.cyber.border, accent: DOMAIN.cyber.accent }
|
|
||||||
: { bg: '#FBECEA', border: '#F3D2CC', accent: '#A23323' }
|
|
||||||
return (
|
|
||||||
<div style={{ flex: 1, minWidth: 200, background: s.bg, border: `1px solid ${s.border}`, borderRadius: 12, padding: '12px 14px' }}>
|
|
||||||
<div className="font-publicSans" style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10.5, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: s.accent }}>
|
|
||||||
<span aria-hidden>{marker}</span>{label}
|
|
||||||
</div>
|
|
||||||
<div className="font-publicSans" style={{ marginTop: 6, fontSize: 12.5, color: COLORS.ink, lineHeight: 1.4 }}>{text}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HazardCard({ link, findings, measures, defaultOpen }: {
|
|
||||||
link: CrossLink; findings: CRAFinding[]; measures: Measure[]; defaultOpen: boolean
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(defaultOpen)
|
|
||||||
const triggers = findings.filter((f) => link.cyber_finding_ids.includes(f.id))
|
|
||||||
const cyberText = triggers.map((f) => f.title).join(' · ') || link.cyber_finding_ids.join(', ')
|
|
||||||
const measureIds = Array.from(new Set(triggers.flatMap((f) => f.measures)))
|
|
||||||
const measureObjs = measureIds.map((id) => measures.find((m) => m.id === id) || { id, name: id, description: '', norm_refs: [] })
|
|
||||||
const normPills = Array.from(new Set(triggers.flatMap((f) => [f.annex_anchor, ...f.requirement_ids, ...f.iso27001_ref]).filter(Boolean)))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ background: COLORS.surface, border: `1px solid ${COLORS.border}`, borderRadius: 16, padding: 20 }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, flexWrap: 'wrap' }}>
|
|
||||||
<div>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 11, color: COLORS.muted2, fontWeight: 600 }}>Mechanische Gefährdung</div>
|
|
||||||
<h3 className="font-publicSans" style={{ fontSize: 17, fontWeight: 800, letterSpacing: '-0.01em', color: COLORS.ink, marginTop: 2 }}>{link.safety_hazard}</h3>
|
|
||||||
</div>
|
|
||||||
<span className="font-publicSans" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#FBECEA', color: '#A23323', border: '1px solid #F3D2CC', borderRadius: 999, padding: '4px 11px', fontSize: 11.5, fontWeight: 700 }}>
|
|
||||||
<span aria-hidden style={{ width: 7, height: 7, borderRadius: 2, background: '#C0362C', display: 'inline-block' }} />
|
|
||||||
Restrisiko: wieder offen
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 14, flexWrap: 'wrap' }}>
|
|
||||||
<ChainNode tone="ce" marker="✓" label="CE — gemildert" text={link.original_measure} />
|
|
||||||
<span aria-hidden style={{ color: '#C5A86F', fontSize: 18 }}>→</span>
|
|
||||||
<ChainNode tone="cyber" marker="⚡" label="Cyber-Befund" text={cyberText} />
|
|
||||||
<span aria-hidden style={{ color: '#C5A86F', fontSize: 18 }}>→</span>
|
|
||||||
<ChainNode tone="residual" marker="!" label="Restrisiko" text="Schutzfunktion aushebelbar — Gefährdung wieder offen" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: 14, background: '#FBFAF7', border: '1px solid #EFEADF', borderRadius: 11, padding: '12px 14px' }}>
|
|
||||||
<span className="font-publicSans" style={{ fontWeight: 700, color: DOMAIN.bridge.warn, fontSize: 12.5 }}>Warum: </span>
|
|
||||||
<span className="font-publicSans" style={{ color: COLORS.ink, fontSize: 13, lineHeight: 1.5 }}>{link.cyber_breaks_it}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{measureObjs.length > 0 && (
|
|
||||||
<div style={{ marginTop: 14 }}>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: COLORS.muted2, marginBottom: 8 }}>Empfohlene Maßnahmen</div>
|
|
||||||
<ul style={{ display: 'grid', gap: 8, margin: 0, padding: 0, listStyle: 'none' }}>
|
|
||||||
{measureObjs.map((m) => (
|
|
||||||
<li key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
||||||
<GeltungChip value={measureGeltung(m.name, m.id)} />
|
|
||||||
<span className="font-publicSans" style={{ flex: 1, fontSize: 13, color: COLORS.ink }}>{m.name}</span>
|
|
||||||
<MonoId>{m.id}</MonoId>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ marginTop: 14, borderTop: `1px solid ${COLORS.borderSoft}`, paddingTop: 12 }}>
|
|
||||||
<button onClick={() => setOpen((v) => !v)} className="font-publicSans" style={{ background: 'none', border: 'none', cursor: 'pointer', color: COLORS.brandText, fontSize: 12.5, fontWeight: 600, padding: 0 }}>
|
|
||||||
{open ? '▾' : '▸'} Auslösende Befunde & Norm-Bezug (Ebene 3)
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 16, marginTop: 12 }}>
|
|
||||||
<div>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: COLORS.muted2, marginBottom: 6 }}>Auslösende Cyber-Befunde</div>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
||||||
{triggers.map((f) => <MonoId key={f.id} className="rounded px-2 py-1" >{f.location || f.id}</MonoId>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: COLORS.muted2, marginBottom: 6 }}>Norm- & Annex-Bezug</div>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
||||||
{normPills.map((n) => (
|
|
||||||
<span key={n} className="font-plexMono" style={{ fontSize: 11, background: COLORS.brandTint, color: COLORS.brandText, borderRadius: 6, padding: '3px 8px' }}>{n}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CyberMeetsSafety({ data }: { data: CRADemo }) {
|
|
||||||
const links = data.cross_links || []
|
|
||||||
const measures = data.open_measures || []
|
|
||||||
const [filter, setFilter] = useState<'alle' | 'pflicht' | 'empfehlung'>('alle')
|
|
||||||
|
|
||||||
if (links.length === 0) {
|
|
||||||
return (
|
|
||||||
<div style={{ background: DOMAIN.cyber.tint, border: `1px solid ${DOMAIN.cyber.border}`, borderRadius: 14, padding: 18 }}>
|
|
||||||
<div className="font-publicSans" style={{ fontWeight: 700, color: COLORS.ink }}>Cyber trifft Safety</div>
|
|
||||||
<p className="font-publicSans" style={{ fontSize: 13, color: COLORS.muted, marginTop: 4 }}>
|
|
||||||
Aktuell keine Cyber-Befunde, die eine CE-Gefährdung wieder öffnen. Sobald Befunde vorliegen, erscheinen sie hier.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const backlog = measures.map((m) => ({ m, g: measureGeltung(m.name, m.id) }))
|
|
||||||
.filter((x) => filter === 'alle' || x.g === filter)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section style={{ display: 'grid', gap: 14 }}>
|
|
||||||
{/* Domänen-Bar */}
|
|
||||||
<div style={{ background: COLORS.surface, border: `1px solid ${COLORS.border}`, borderRadius: 14, padding: 16, display: 'flex', gap: 12, alignItems: 'stretch', flexWrap: 'wrap' }}>
|
|
||||||
<div style={{ flex: 1, minWidth: 180, borderLeft: `4px solid ${DOMAIN.safety.accent}`, paddingLeft: 12 }}>
|
|
||||||
<div className="font-publicSans" style={{ fontWeight: 700, color: DOMAIN.safety.accent, fontSize: 13 }}>Safety (Maschine / CE)</div>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 12, color: COLORS.muted, marginTop: 2 }}>Mechanisch gemilderte Gefährdungen</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ minWidth: 180, background: DOMAIN.bridge.tint, border: `1px solid ${DOMAIN.bridge.border}`, borderRadius: 10, padding: '10px 14px', textAlign: 'center' }}>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 20 }} aria-hidden>⚡</div>
|
|
||||||
<div className="font-publicSans" style={{ fontWeight: 800, color: DOMAIN.bridge.warn, fontSize: 14 }}>{links.length} wieder geöffnet</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 180, borderLeft: `4px solid ${DOMAIN.cyber.accent}`, paddingLeft: 12 }}>
|
|
||||||
<div className="font-publicSans" style={{ fontWeight: 700, color: DOMAIN.cyber.accent, fontSize: 13 }}>Cyber (CRA)</div>
|
|
||||||
<div className="font-publicSans" style={{ fontSize: 12, color: COLORS.muted, marginTop: 2 }}>Befunde, die Schutzfunktionen aushebeln</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{links.map((link, i) => (
|
|
||||||
<HazardCard key={i} link={link} findings={data.findings} measures={measures} defaultOpen={i === 0} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Maßnahmen-Backlog */}
|
|
||||||
<div style={{ background: COLORS.surface, border: `1px solid ${COLORS.border}`, borderRadius: 14, padding: 18 }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
|
||||||
<div className="font-publicSans" style={{ fontWeight: 800, color: COLORS.ink, fontSize: 15 }}>
|
|
||||||
Maßnahmen-Backlog <span style={{ fontWeight: 500, color: COLORS.faint }}>· {measures.length} Maßnahmen · nach Geltung</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 4, background: COLORS.page, borderRadius: 8, padding: 3 }}>
|
|
||||||
{(['alle', 'pflicht', 'empfehlung'] as const).map((f) => (
|
|
||||||
<button key={f} onClick={() => setFilter(f)} className="font-publicSans"
|
|
||||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: 'none', cursor: 'pointer', borderRadius: 6, padding: '4px 10px', fontSize: 12, fontWeight: 600,
|
|
||||||
background: filter === f ? COLORS.surface : 'transparent', color: filter === f ? COLORS.ink : COLORS.muted }}>
|
|
||||||
{filter === f && <span aria-hidden style={{ width: 6, height: 6, borderRadius: 999, background: COLORS.brand, display: 'inline-block' }} />}
|
|
||||||
{f === 'alle' ? 'Alle' : f === 'pflicht' ? 'Pflicht' : 'Empfehlung'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul style={{ marginTop: 12, display: 'grid', gap: 2, padding: 0, listStyle: 'none' }}>
|
|
||||||
{backlog.map(({ m, g }) => (
|
|
||||||
<li key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderTop: `1px solid ${COLORS.borderSoft}` }}>
|
|
||||||
<span style={{ width: 96 }}><GeltungChip value={g} /></span>
|
|
||||||
<span className="font-publicSans" style={{ flex: 1, fontSize: 13, color: COLORS.ink }}>{m.name}</span>
|
|
||||||
<span className="font-publicSans" style={{ width: 200, fontSize: 12, color: COLORS.faint }}>{m.norm_refs?.[0] || 'Sicherheit'}</span>
|
|
||||||
<MonoId>{m.id}</MonoId>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{backlog.length === 0 && <li className="font-publicSans" style={{ fontSize: 13, color: COLORS.faint, paddingTop: 8 }}>Keine Maßnahmen in diesem Filter.</li>}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useCRA } from './_hooks/useCRA'
|
import { useCRA } from './_hooks/useCRA'
|
||||||
import { CRACyberView } from './_components/CRACyberView'
|
import { CRACyberView } from './_components/CRACyberView'
|
||||||
import { CyberMeetsSafety } from './_components/CyberMeetsSafety'
|
|
||||||
import { WeightsControl } from './_components/WeightsControl'
|
import { WeightsControl } from './_components/WeightsControl'
|
||||||
import { SnapshotPanel } from './_components/SnapshotPanel'
|
import { SnapshotPanel } from './_components/SnapshotPanel'
|
||||||
import { ScannerRepoPicker } from './_components/ScannerRepoPicker'
|
import { ScannerRepoPicker } from './_components/ScannerRepoPicker'
|
||||||
@@ -32,22 +31,7 @@ export default function CRAPage() {
|
|||||||
)}
|
)}
|
||||||
<ScannerRepoPicker value={scannerRepo} onChange={setScannerRepo} />
|
<ScannerRepoPicker value={scannerRepo} onChange={setScannerRepo} />
|
||||||
<WeightsControl weights={weights} onChange={setWeights} />
|
<WeightsControl weights={weights} onChange={setWeights} />
|
||||||
|
|
||||||
{/* Ebene 2 — Cyber trifft Safety (Redesign, neue Design-Sprache) */}
|
|
||||||
<div>
|
|
||||||
<h2 className="font-publicSans" style={{ fontSize: 19, fontWeight: 800, letterSpacing: '-0.01em', color: '#1A1D29', marginBottom: 12 }}>
|
|
||||||
Cyber trifft Safety
|
|
||||||
</h2>
|
|
||||||
<CyberMeetsSafety data={data} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bisherige Detailansicht (bleibt erhalten, bis das Redesign 100% abdeckt) */}
|
|
||||||
<details className="rounded-lg border border-gray-200 dark:border-gray-700">
|
|
||||||
<summary className="cursor-pointer px-4 py-2 text-sm text-gray-500">Bisherige Detailansicht (CRACyberView)</summary>
|
|
||||||
<div className="p-1">
|
|
||||||
<CRACyberView data={data} scannerRepo={scannerRepo} />
|
<CRACyberView data={data} scannerRepo={scannerRepo} />
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<SnapshotPanel snapshots={snapshots} onSave={saveSnapshot} onView={viewSnapshot} />
|
<SnapshotPanel snapshots={snapshots} onSave={saveSnapshot} onView={viewSnapshot} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
// Reusable design-language chips. Geltung (Pflicht/Empfehlung/Kann) and Severity
|
|
||||||
// follow the handoff specs exactly (bg/text/border + marker glyph). Klartext label
|
|
||||||
// leads; internal IDs are never shown by the chip itself. See ./tokens.
|
|
||||||
|
|
||||||
import {
|
|
||||||
GELTUNG, PFLICHT_MARKER, SEVERITY, DOMAIN,
|
|
||||||
normalizeGeltung, normalizeSeverity, Geltung, Severity, Domain,
|
|
||||||
} from './tokens'
|
|
||||||
|
|
||||||
function GeltungMarker({ g }: { g: Geltung }) {
|
|
||||||
const m = GELTUNG[g].marker
|
|
||||||
if (m === 'square') {
|
|
||||||
return <span aria-hidden style={{ width: 7, height: 7, borderRadius: 2, background: PFLICHT_MARKER, display: 'inline-block' }} />
|
|
||||||
}
|
|
||||||
// diamond ◇ (Empfehlung) / circle ○ (Kann) — open glyphs in the chip text color
|
|
||||||
return <span aria-hidden style={{ fontSize: 10, lineHeight: 1 }}>{m === 'diamond' ? '◇' : '○'}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GeltungChip({ value, className = '' }: { value: Geltung | string; className?: string }) {
|
|
||||||
const g = normalizeGeltung(value)
|
|
||||||
const t = GELTUNG[g]
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center gap-1.5 rounded-md font-publicSans font-bold ${className}`}
|
|
||||||
style={{ background: t.bg, color: t.text, border: `1px solid ${t.border}`, padding: '3px 9px', fontSize: 11 }}
|
|
||||||
>
|
|
||||||
<GeltungMarker g={g} />
|
|
||||||
{t.label}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SeverityChip({ value, className = '' }: { value: Severity | string; className?: string }) {
|
|
||||||
const s = normalizeSeverity(value)
|
|
||||||
const t = SEVERITY[s]
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-[7px] font-publicSans font-bold ${className}`}
|
|
||||||
style={{ background: t.bg, color: t.text, padding: '4px 11px', fontSize: 12 }}
|
|
||||||
>
|
|
||||||
{t.label}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small domain tag (Safety / Cyber / Schnittstelle) — tinted, not neon.
|
|
||||||
export function DomainTag({ value, label, className = '' }: { value: Domain; label?: string; className?: string }) {
|
|
||||||
const d = DOMAIN[value]
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-md font-publicSans font-semibold ${className}`}
|
|
||||||
style={{ background: d.tint, color: d.warn || d.accent, border: `1px solid ${d.border}`, padding: '3px 9px', fontSize: 11 }}
|
|
||||||
>
|
|
||||||
{label || d.label}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monospace internal ID — IDs are ALWAYS secondary/nachgestellt, never a heading.
|
|
||||||
export function MonoId({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
|
||||||
return <span className={`font-plexMono ${className}`} style={{ fontSize: 11.5, color: '#A2A8B8' }}>{children}</span>
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Design-language tokens — single source of truth for the redesign.
|
|
||||||
// Mirrors design/redesign/HANDOFF_README.md (Claude Design handoff). The same
|
|
||||||
// values are mirrored into tailwind.config.ts (namespaces re/geltung/severity/domain)
|
|
||||||
// for utility-class use; components that need the exact chip look import from here.
|
|
||||||
|
|
||||||
export const COLORS = {
|
|
||||||
page: '#EDEFF3', surface: '#FFFFFF', border: '#E4E7EE', borderSoft: '#F0F1F5',
|
|
||||||
ink: '#1A1D29', muted: '#5A6273', muted2: '#6B7184', faint: '#8089A0', fainter: '#9AA1B2',
|
|
||||||
brand: '#4338CA', brandText: '#3B36B0', brandTint: '#EEF0FF', brandTint2: '#F6F4FF',
|
|
||||||
panel: '#15182A', panelText: '#E8EAF2', panelText2: '#C7CBDA', panelAccent: '#9B8BF5',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// --- Geltung: Pflicht / Empfehlung / Kann (the core 3-tier obligation system) ---
|
|
||||||
export type Geltung = 'pflicht' | 'empfehlung' | 'kann'
|
|
||||||
export const GELTUNG: Record<Geltung, {
|
|
||||||
label: string; bg: string; text: string; border: string; marker: 'square' | 'diamond' | 'circle'
|
|
||||||
}> = {
|
|
||||||
pflicht: { label: 'Pflicht', bg: '#FBECEA', text: '#A23323', border: '#F3D2CC', marker: 'square' },
|
|
||||||
empfehlung: { label: 'Empfehlung', bg: '#EEF0FF', text: '#3B36B0', border: '#DAD9F7', marker: 'diamond' },
|
|
||||||
kann: { label: 'Kann', bg: '#F1F3F7', text: '#5A6273', border: '#E2E6EE', marker: 'circle' },
|
|
||||||
}
|
|
||||||
export const PFLICHT_MARKER = '#C0362C' // filled square color
|
|
||||||
|
|
||||||
export function normalizeGeltung(v: string | Geltung): Geltung {
|
|
||||||
const s = String(v || '').toLowerCase()
|
|
||||||
if (['pflicht', 'mandatory', 'required', 'must', 'core'].includes(s)) return 'pflicht'
|
|
||||||
if (['empfehlung', 'recommended', 'should', 'review'].includes(s)) return 'empfehlung'
|
|
||||||
if (['kann', 'optional', 'may', 'can'].includes(s)) return 'kann'
|
|
||||||
return 'empfehlung' // unknown → recommendation (never silently drop; never over-state as Pflicht)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Severity (Dringlichkeit) ---
|
|
||||||
export type Severity = 'kritisch' | 'hoch' | 'mittel' | 'niedrig'
|
|
||||||
export const SEVERITY: Record<Severity, { label: string; bg: string; text: string }> = {
|
|
||||||
kritisch: { label: 'Kritisch', bg: '#FBE9E7', text: '#B5362A' },
|
|
||||||
hoch: { label: 'Hoch', bg: '#FBF1E0', text: '#9A6410' },
|
|
||||||
mittel: { label: 'Mittel', bg: '#FAF6DD', text: '#897209' },
|
|
||||||
niedrig: { label: 'Niedrig', bg: '#E9F5EF', text: '#2C7A52' },
|
|
||||||
}
|
|
||||||
export function normalizeSeverity(v: string | Severity): Severity {
|
|
||||||
const s = String(v || '').toLowerCase()
|
|
||||||
if (['kritisch', 'critical'].includes(s)) return 'kritisch'
|
|
||||||
if (['hoch', 'high'].includes(s)) return 'hoch'
|
|
||||||
if (['mittel', 'medium', 'moderate'].includes(s)) return 'mittel'
|
|
||||||
if (['niedrig', 'low', 'minor'].includes(s)) return 'niedrig'
|
|
||||||
return 'mittel'
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Domains (Safety / Cyber / Schnittstelle) ---
|
|
||||||
export type Domain = 'safety' | 'cyber' | 'bridge'
|
|
||||||
export const DOMAIN: Record<Domain, { label: string; accent: string; tint: string; border: string; warn?: string }> = {
|
|
||||||
safety: { label: 'Safety (Maschine/CE)', accent: '#0E8A66', tint: '#F3FAF7', border: '#D7ECE3' },
|
|
||||||
cyber: { label: 'Cyber (CRA)', accent: '#6A43D6', tint: '#F6F1FE', border: '#E4D8F7' },
|
|
||||||
bridge: { label: 'Cyber × Safety', accent: '#BE7714', tint: '#FCF6EF', border: '#F2E6D5', warn: '#9A6410' },
|
|
||||||
}
|
|
||||||
@@ -48,39 +48,9 @@ const config: Config = {
|
|||||||
900: '#0c4a6e',
|
900: '#0c4a6e',
|
||||||
950: '#082f49',
|
950: '#082f49',
|
||||||
},
|
},
|
||||||
|
|
||||||
// === Redesign design-language tokens (2026-06, see design/redesign) ===
|
|
||||||
// Additive + namespaced ('re', 'geltung', 'severity', 'domain') so nothing
|
|
||||||
// existing is overridden. Single source of truth: components/redesign/tokens.ts.
|
|
||||||
re: {
|
|
||||||
page: '#EDEFF3', surface: '#FFFFFF', border: '#E4E7EE', 'border-soft': '#F0F1F5',
|
|
||||||
ink: '#1A1D29', muted: '#5A6273', 'muted-2': '#6B7184', faint: '#8089A0', fainter: '#9AA1B2',
|
|
||||||
brand: '#4338CA', 'brand-text': '#3B36B0', 'brand-tint': '#EEF0FF', 'brand-tint-2': '#F6F4FF',
|
|
||||||
panel: '#15182A', 'panel-text': '#E8EAF2', 'panel-text-2': '#C7CBDA', 'panel-accent': '#9B8BF5',
|
|
||||||
},
|
|
||||||
geltung: {
|
|
||||||
pflicht: { bg: '#FBECEA', text: '#A23323', border: '#F3D2CC', marker: '#C0362C' },
|
|
||||||
empfehlung: { bg: '#EEF0FF', text: '#3B36B0', border: '#DAD9F7' },
|
|
||||||
kann: { bg: '#F1F3F7', text: '#5A6273', border: '#E2E6EE' },
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
kritisch: { bg: '#FBE9E7', text: '#B5362A' },
|
|
||||||
hoch: { bg: '#FBF1E0', text: '#9A6410' },
|
|
||||||
mittel: { bg: '#FAF6DD', text: '#897209' },
|
|
||||||
niedrig: { bg: '#E9F5EF', text: '#2C7A52' },
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
safety: '#0E8A66', 'safety-tint': '#F3FAF7', 'safety-border': '#D7ECE3',
|
|
||||||
cyber: '#6A43D6', 'cyber-tint': '#F6F1FE', 'cyber-border': '#E4D8F7',
|
|
||||||
bridge: '#BE7714', 'bridge-tint': '#FCF6EF', 'bridge-border': '#F2E6D5', 'bridge-warn': '#9A6410',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
// Redesign fonts (loaded via next/font in app/layout.tsx as CSS variables).
|
|
||||||
publicSans: ['var(--font-public-sans)', 'Inter', 'system-ui', 'sans-serif'],
|
|
||||||
sourceSerif: ['var(--font-source-serif)', 'Georgia', 'serif'],
|
|
||||||
plexMono: ['var(--font-plex-mono)', 'ui-monospace', 'SFMono-Regular', 'monospace'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user