chore: Add development screens, update navigation and docker-compose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-12 20:29:27 +01:00
parent d9c13c947b
commit 0923c03756
7 changed files with 2681 additions and 12 deletions

View File

@@ -0,0 +1,622 @@
'use client'
/**
* Screen Flow Visualization - Compliance SDK
*
* Visualisiert alle Screens aus:
* - Admin Compliance (Port 3007): Verwaltung
* - SDK Pipeline: Compliance-Module
*/
import { useCallback, useState, useMemo, useEffect } from 'react'
import ReactFlow, {
Node,
Edge,
Controls,
Background,
MiniMap,
useNodesState,
useEdgesState,
BackgroundVariant,
MarkerType,
Panel,
} from 'reactflow'
import 'reactflow/dist/style.css'
// ============================================
// TYPES
// ============================================
interface ScreenDefinition {
id: string
name: string
description: string
category: string
icon: string
url?: string
}
interface ConnectionDef {
source: string
target: string
label?: string
}
// ============================================
// ADMIN COMPLIANCE SCREENS (Port 3007)
// ============================================
const SCREENS: ScreenDefinition[] = [
// === DASHBOARD & VERWALTUNG (Blue) ===
{ id: 'dashboard', name: 'Dashboard', description: 'Uebersicht & Statistiken', category: 'dashboard', icon: '🏠', url: '/dashboard' },
{ id: 'catalog-manager', name: 'Katalogverwaltung', description: 'SDK-Kataloge & Auswahltabellen', category: 'dashboard', icon: '📦', url: '/dashboard/catalog-manager' },
// === DSGVO-GRUNDLAGEN (Violet) ===
{ id: 'vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'dsgvo', icon: '📋', url: '/sdk/vvt' },
{ id: 'dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'dsgvo', icon: '⚖️', url: '/sdk/dsfa' },
{ id: 'tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'dsgvo', icon: '🛡️', url: '/sdk/tom' },
{ id: 'tom-generator', name: 'TOM Generator', description: 'TOM-Erstellung mit Wizard', category: 'dsgvo', icon: '⚙️', url: '/sdk/tom-generator' },
{ id: 'loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'dsgvo', icon: '🗑️', url: '/sdk/loeschfristen' },
{ id: 'einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'dsgvo', icon: '✅', url: '/sdk/einwilligungen' },
{ id: 'dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21 (DSR)', category: 'dsgvo', icon: '🔒', url: '/sdk/dsr' },
{ id: 'consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'dsgvo', icon: '📄', url: '/sdk/consent' },
{ id: 'consent-management', name: 'Consent Management', description: 'Einwilligungsmanagement', category: 'dsgvo', icon: '📝', url: '/sdk/consent-management' },
// === COMPLIANCE-MANAGEMENT (Purple) ===
{ id: 'compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'compliance', icon: '✅', url: '/sdk/compliance-hub' },
{ id: 'compliance-scope', name: 'Compliance Scope', description: 'Geltungsbereich definieren', category: 'compliance', icon: '🎯', url: '/sdk/compliance-scope' },
{ id: 'requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'compliance', icon: '📜', url: '/sdk/requirements' },
{ id: 'controls', name: 'Controls', description: '474 Control-Mappings', category: 'compliance', icon: '🎛️', url: '/sdk/controls' },
{ id: 'evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'compliance', icon: '📎', url: '/sdk/evidence' },
{ id: 'risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'compliance', icon: '⚠️', url: '/sdk/risks' },
{ id: 'audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'compliance', icon: '📋', url: '/sdk/audit-checklist' },
{ id: 'audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'compliance', icon: '📊', url: '/sdk/audit-report' },
{ id: 'workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'compliance', icon: '🔄', url: '/sdk/workflow' },
{ id: 'modules', name: 'Service Registry', description: '30+ Service-Module', category: 'compliance', icon: '🔧', url: '/sdk/modules' },
// === KI & AUTOMATISIERUNG (Teal) ===
{ id: 'ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'ai', icon: '🤖', url: '/sdk/ai-act' },
{ id: 'screening', name: 'Screening', description: 'Compliance-Screening & Pruefung', category: 'ai', icon: '🔍', url: '/sdk/screening' },
{ id: 'rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/sdk/rag' },
{ id: 'quality', name: 'Qualitaet & Audit', description: 'Compliance-Audit & Traceability', category: 'ai', icon: '✨', url: '/sdk/quality' },
{ id: 'advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'ai', icon: '🧑‍⚖️', url: '/sdk/advisory-board' },
{ id: 'obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'ai', icon: '⚡', url: '/sdk/obligations' },
{ id: 'escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'ai', icon: '🚨', url: '/sdk/escalations' },
// === DOKUMENTE & LEGAL (Orange) ===
{ id: 'document-generator', name: 'Document Generator', description: 'Datenschutz-Dokumente erstellen', category: 'documents', icon: '📄', url: '/sdk/document-generator' },
{ id: 'notfallplan', name: 'Notfallplan', description: 'Incident Response Plan', category: 'documents', icon: '🚨', url: '/sdk/notfallplan' },
{ id: 'source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'documents', icon: '📚', url: '/sdk/source-policy' },
{ id: 'cookie-banner', name: 'Cookie-Banner', description: 'Cookie Consent Builder', category: 'documents', icon: '🍪', url: '/sdk/cookie-banner' },
{ id: 'company-profile', name: 'Unternehmensprofil', description: 'Firmen-Stammdaten', category: 'documents', icon: '🏢', url: '/sdk/company-profile' },
{ id: 'security-backlog', name: 'Security Backlog', description: 'Sicherheits-Massnahmen Tracking', category: 'documents', icon: '🔐', url: '/sdk/security-backlog' },
// === VENDOR & EXTERN (Green) ===
{ id: 'vendor-compliance', name: 'Vendor Compliance', description: 'Lieferanten-Management', category: 'vendor', icon: '🏭', url: '/sdk/vendor-compliance' },
{ id: 'vendor-vendors', name: 'Vendor-Liste', description: 'Lieferanten-Uebersicht', category: 'vendor', icon: '📋', url: '/sdk/vendor-compliance/vendors' },
{ id: 'vendor-contracts', name: 'Vertraege', description: 'AVV & Vertragsmanagement', category: 'vendor', icon: '📝', url: '/sdk/vendor-compliance/contracts' },
{ id: 'vendor-controls', name: 'Vendor Controls', description: 'Lieferanten-Kontrollen', category: 'vendor', icon: '🎛️', url: '/sdk/vendor-compliance/controls' },
{ id: 'vendor-risks', name: 'Vendor Risiken', description: 'Lieferanten-Risikobewertung', category: 'vendor', icon: '⚠️', url: '/sdk/vendor-compliance/risks' },
{ id: 'vendor-processing', name: 'Verarbeitungen', description: 'Auftragsverarbeitung', category: 'vendor', icon: '🔄', url: '/sdk/vendor-compliance/processing-activities' },
{ id: 'vendor-reports', name: 'Vendor Reports', description: 'Lieferanten-Berichte', category: 'vendor', icon: '📊', url: '/sdk/vendor-compliance/reports' },
{ id: 'dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'vendor', icon: '🏛️', url: '/sdk/dsms' },
{ id: 'import', name: 'Import', description: 'Daten-Import', category: 'vendor', icon: '📥', url: '/sdk/import' },
// === ENTWICKLUNG (Slate) ===
{ id: 'dev-docs', name: 'Developer Docs', description: 'API & Architektur', category: 'development', icon: '📖', url: '/development/docs' },
{ id: 'dev-screen-flow', name: 'Screen Flow', description: 'UI Screen-Verbindungen', category: 'development', icon: '🔀', url: '/development/screen-flow' },
{ id: 'dev-brandbook', name: 'Brandbook', description: 'Corporate Design', category: 'development', icon: '🎨', url: '/development/brandbook' },
]
const CONNECTIONS: ConnectionDef[] = [
// === DASHBOARD FLOW ===
{ source: 'dashboard', target: 'catalog-manager', label: 'Kataloge' },
{ source: 'dashboard', target: 'compliance-hub', label: 'Compliance' },
{ source: 'dashboard', target: 'vvt', label: 'VVT' },
// === DSGVO FLOW ===
{ source: 'consent-management', target: 'einwilligungen', label: 'Nutzer' },
{ source: 'consent-management', target: 'dsr' },
{ source: 'consent', target: 'consent-management' },
{ source: 'consent', target: 'cookie-banner' },
{ source: 'dsr', target: 'loeschfristen' },
{ source: 'vvt', target: 'tom' },
{ source: 'vvt', target: 'dsfa' },
{ source: 'dsfa', target: 'tom' },
{ source: 'tom', target: 'tom-generator', label: 'Wizard' },
{ source: 'einwilligungen', target: 'consent' },
{ source: 'einwilligungen', target: 'loeschfristen' },
// === COMPLIANCE FLOW ===
{ source: 'compliance-hub', target: 'audit-checklist', label: 'Audit' },
{ source: 'compliance-hub', target: 'requirements', label: 'Anforderungen' },
{ source: 'compliance-hub', target: 'risks', label: 'Risiken' },
{ source: 'compliance-hub', target: 'ai-act', label: 'AI Act' },
{ source: 'compliance-hub', target: 'compliance-scope' },
{ source: 'requirements', target: 'controls' },
{ source: 'controls', target: 'evidence' },
{ source: 'audit-checklist', target: 'audit-report', label: 'Report' },
{ source: 'risks', target: 'controls' },
{ source: 'modules', target: 'controls' },
{ source: 'obligations', target: 'requirements' },
{ source: 'dsms', target: 'workflow' },
{ source: 'workflow', target: 'audit-report' },
// === KI & AUTOMATISIERUNG FLOW ===
{ source: 'advisory-board', target: 'escalations', label: 'Eskalation' },
{ source: 'advisory-board', target: 'dsfa', label: 'Risiko' },
{ source: 'ai-act', target: 'screening' },
{ source: 'screening', target: 'advisory-board' },
{ source: 'source-policy', target: 'rag' },
{ source: 'rag', target: 'quality' },
// === DOKUMENTE FLOW ===
{ source: 'document-generator', target: 'notfallplan' },
{ source: 'document-generator', target: 'audit-report' },
{ source: 'security-backlog', target: 'tom' },
{ source: 'company-profile', target: 'document-generator' },
// === VENDOR FLOW ===
{ source: 'vendor-compliance', target: 'vendor-vendors' },
{ source: 'vendor-compliance', target: 'vendor-contracts' },
{ source: 'vendor-compliance', target: 'vendor-controls' },
{ source: 'vendor-compliance', target: 'vendor-risks' },
{ source: 'vendor-compliance', target: 'vendor-processing' },
{ source: 'vendor-compliance', target: 'vendor-reports' },
{ source: 'vendor-vendors', target: 'vendor-contracts' },
{ source: 'vendor-risks', target: 'risks' },
{ source: 'dsms', target: 'compliance-hub' },
{ source: 'import', target: 'catalog-manager' },
// === ENTWICKLUNG FLOW ===
{ source: 'dev-brandbook', target: 'dev-screen-flow' },
{ source: 'dev-docs', target: 'dev-brandbook' },
]
// ============================================
// CATEGORY COLORS & LABELS
// ============================================
const COLORS: Record<string, { bg: string; border: string; text: string }> = {
dashboard: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
dsgvo: { bg: '#ede9fe', border: '#7c3aed', text: '#5b21b6' },
compliance: { bg: '#f3e8ff', border: '#9333ea', text: '#6b21a8' },
ai: { bg: '#ccfbf1', border: '#14b8a6', text: '#0f766e' },
documents: { bg: '#ffedd5', border: '#f97316', text: '#c2410c' },
vendor: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
development: { bg: '#f1f5f9', border: '#64748b', text: '#334155' },
}
const LABELS: Record<string, string> = {
dashboard: 'Dashboard & Verwaltung',
dsgvo: 'DSGVO-Grundlagen',
compliance: 'Compliance-Management',
ai: 'KI & Automatisierung',
documents: 'Dokumente & Legal',
vendor: 'Vendor & Extern',
development: 'Entwicklung',
}
// ============================================
// HELPER: Find all connected nodes
// ============================================
function findConnectedNodes(
startNodeId: string,
connections: ConnectionDef[],
direction: 'children' | 'parents' | 'both' = 'children'
): Set<string> {
const connected = new Set<string>()
connected.add(startNodeId)
const queue = [startNodeId]
while (queue.length > 0) {
const current = queue.shift()!
connections.forEach(conn => {
if ((direction === 'children' || direction === 'both') && conn.source === current) {
if (!connected.has(conn.target)) {
connected.add(conn.target)
queue.push(conn.target)
}
}
if ((direction === 'parents' || direction === 'both') && conn.target === current) {
if (!connected.has(conn.source)) {
connected.add(conn.source)
queue.push(conn.source)
}
}
})
}
return connected
}
// ============================================
// LAYOUT HELPERS
// ============================================
const CATEGORY_POSITIONS: Record<string, { x: number; y: number }> = {
dashboard: { x: 400, y: 30 },
dsgvo: { x: 50, y: 150 },
compliance: { x: 700, y: 150 },
ai: { x: 50, y: 380 },
documents: { x: 400, y: 380 },
vendor: { x: 700, y: 380 },
development: { x: 400, y: 580 },
}
const getNodePosition = (id: string, category: string) => {
const base = CATEGORY_POSITIONS[category] || { x: 400, y: 300 }
const categoryScreens = SCREENS.filter(s => s.category === category)
const categoryIndex = categoryScreens.findIndex(s => s.id === id)
const cols = Math.ceil(Math.sqrt(categoryScreens.length + 1))
const row = Math.floor(categoryIndex / cols)
const col = categoryIndex % cols
return {
x: base.x + col * 160,
y: base.y + row * 90,
}
}
// ============================================
// MAIN COMPONENT
// ============================================
export default function ScreenFlowPage() {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [selectedNode, setSelectedNode] = useState<string | null>(null)
const [previewScreen, setPreviewScreen] = useState<ScreenDefinition | null>(null)
const baseUrl = 'https://macmini:3007'
// Calculate connected nodes
const connectedNodes = useMemo(() => {
if (!selectedNode) return new Set<string>()
return findConnectedNodes(selectedNode, CONNECTIONS, 'children')
}, [selectedNode])
// Create nodes with useMemo
const initialNodes = useMemo((): Node[] => {
return SCREENS.map((screen) => {
const catColors = COLORS[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
const position = getNodePosition(screen.id, screen.category)
let opacity = 1
if (selectedNode) {
opacity = connectedNodes.has(screen.id) ? 1 : 0.2
} else if (selectedCategory) {
opacity = screen.category === selectedCategory ? 1 : 0.2
}
const isSelected = selectedNode === screen.id
return {
id: screen.id,
type: 'default',
position,
data: {
label: (
<div className="text-center p-1">
<div className="text-lg mb-1">{screen.icon}</div>
<div className="font-medium text-xs leading-tight">{screen.name}</div>
</div>
),
},
style: {
background: isSelected ? catColors.border : catColors.bg,
color: isSelected ? 'white' : catColors.text,
border: `2px solid ${catColors.border}`,
borderRadius: '12px',
padding: '6px',
minWidth: '110px',
opacity,
cursor: 'pointer',
boxShadow: isSelected ? `0 0 20px ${catColors.border}` : 'none',
},
}
})
}, [selectedCategory, selectedNode, connectedNodes])
// Create edges with useMemo
const initialEdges = useMemo((): Edge[] => {
return CONNECTIONS.map((conn, index) => {
const isHighlighted = selectedNode && (conn.source === selectedNode || conn.target === selectedNode)
const isInSubtree = selectedNode && connectedNodes.has(conn.source) && connectedNodes.has(conn.target)
return {
id: `e-${conn.source}-${conn.target}-${index}`,
source: conn.source,
target: conn.target,
label: conn.label,
type: 'smoothstep',
animated: isHighlighted || false,
style: {
stroke: isHighlighted ? '#3b82f6' : (isInSubtree ? '#94a3b8' : '#e2e8f0'),
strokeWidth: isHighlighted ? 3 : 1.5,
opacity: selectedNode ? (isInSubtree ? 1 : 0.15) : 1,
},
labelStyle: { fontSize: 9, fill: '#64748b' },
labelBgStyle: { fill: '#f8fafc' },
markerEnd: { type: MarkerType.ArrowClosed, color: isHighlighted ? '#3b82f6' : '#94a3b8', width: 15, height: 15 },
}
})
}, [selectedNode, connectedNodes])
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
// Update nodes/edges when dependencies change
useEffect(() => {
setNodes(initialNodes)
setEdges(initialEdges)
}, [initialNodes, initialEdges, setNodes, setEdges])
// Handle node click
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const screen = SCREENS.find(s => s.id === node.id)
if (selectedNode === node.id) {
if (screen?.url) {
window.open(`${baseUrl}${screen.url}`, '_blank')
}
return
}
setSelectedNode(node.id)
setSelectedCategory(null)
if (screen) {
setPreviewScreen(screen)
}
}, [selectedNode, baseUrl])
// Handle background click - deselect
const onPaneClick = useCallback(() => {
setSelectedNode(null)
setPreviewScreen(null)
}, [])
// Stats
const stats = {
totalScreens: SCREENS.length,
totalConnections: CONNECTIONS.length,
connectedCount: connectedNodes.size,
}
const categories = Object.keys(LABELS)
// Connected screens list
const connectedScreens = selectedNode
? SCREENS.filter(s => connectedNodes.has(s.id))
: []
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-xl bg-violet-500 flex items-center justify-center text-2xl text-white">
🔀
</div>
<div>
<h2 className="text-xl font-bold text-slate-900">Compliance SDK Screen Flow</h2>
<p className="text-sm text-slate-500">
{stats.totalScreens} Screens mit {stats.totalConnections} Verbindungen
</p>
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-slate-800">{stats.totalScreens}</div>
<div className="text-sm text-slate-500">Screens</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-violet-600">{stats.totalConnections}</div>
<div className="text-sm text-slate-500">Verbindungen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm col-span-2">
{selectedNode ? (
<div className="flex items-center gap-3">
<div className="text-3xl">{previewScreen?.icon}</div>
<div>
<div className="font-bold text-slate-800">{previewScreen?.name}</div>
<div className="text-sm text-slate-500">
{stats.connectedCount} verbundene Screen{stats.connectedCount !== 1 ? 's' : ''}
</div>
</div>
<button
onClick={() => {
setSelectedNode(null)
setPreviewScreen(null)
}}
className="ml-auto px-3 py-1 text-sm bg-slate-100 hover:bg-slate-200 rounded-lg"
>
Zuruecksetzen
</button>
</div>
) : (
<div className="text-slate-500 text-sm">
Klicke auf einen Screen um den Subtree zu sehen
</div>
)}
</div>
</div>
{/* Category Filter */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex flex-wrap gap-2">
<button
onClick={() => {
setSelectedCategory(null)
setSelectedNode(null)
setPreviewScreen(null)
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === null && !selectedNode
? 'bg-slate-800 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Alle ({SCREENS.length})
</button>
{categories.map((key) => {
const count = SCREENS.filter(s => s.category === key).length
const catColors = COLORS[key] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
return (
<button
key={key}
onClick={() => {
setSelectedCategory(selectedCategory === key ? null : key)
setSelectedNode(null)
setPreviewScreen(null)
}}
className="px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2"
style={{
background: selectedCategory === key ? catColors.border : catColors.bg,
color: selectedCategory === key ? 'white' : catColors.text,
}}
>
<span className="w-3 h-3 rounded-full" style={{ background: catColors.border }} />
{LABELS[key]} ({count})
</button>
)
})}
</div>
</div>
{/* Connected Screens List */}
{selectedNode && connectedScreens.length > 1 && (
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-sm font-medium text-slate-700 mb-3">Verbundene Screens:</div>
<div className="flex flex-wrap gap-2">
{connectedScreens.map((screen) => {
const catColors = COLORS[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
const isCurrentNode = screen.id === selectedNode
return (
<button
key={screen.id}
onClick={() => {
if (screen.url) {
window.open(`${baseUrl}${screen.url}`, '_blank')
}
}}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${
isCurrentNode ? 'ring-2 ring-violet-500' : ''
}`}
style={{
background: isCurrentNode ? catColors.border : catColors.bg,
color: isCurrentNode ? 'white' : catColors.text,
}}
>
<span>{screen.icon}</span>
{screen.name}
</button>
)
})}
</div>
</div>
)}
{/* Flow Diagram */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '500px' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
fitView
fitViewOptions={{ padding: 0.2 }}
attributionPosition="bottom-left"
>
<Controls />
<MiniMap
nodeColor={(node) => {
const screen = SCREENS.find(s => s.id === node.id)
const catColors = screen ? COLORS[screen.category] : null
return catColors?.border || '#94a3b8'
}}
maskColor="rgba(0, 0, 0, 0.1)"
/>
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
<Panel position="top-left" className="bg-white/95 p-3 rounded-lg shadow-lg text-xs">
<div className="font-medium text-slate-700 mb-2">
🛡 Compliance SDK
</div>
<div className="space-y-1">
{categories.slice(0, 5).map((key) => {
const catColors = COLORS[key] || { bg: '#f1f5f9', border: '#94a3b8' }
return (
<div key={key} className="flex items-center gap-2">
<span
className="w-3 h-3 rounded"
style={{ background: catColors.bg, border: `1px solid ${catColors.border}` }}
/>
<span className="text-slate-600">{LABELS[key]}</span>
</div>
)
})}
</div>
<div className="mt-2 pt-2 border-t text-slate-400">
Klick = Subtree<br/>
Doppelklick = Oeffnen
</div>
</Panel>
</ReactFlow>
</div>
{/* Screen List */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-4 py-3 bg-slate-50 border-b flex items-center justify-between">
<h3 className="font-medium text-slate-700">
Alle Screens ({SCREENS.length})
</h3>
<span className="text-xs text-slate-400">{baseUrl}</span>
</div>
<div className="divide-y max-h-80 overflow-y-auto">
{SCREENS
.filter(s => !selectedCategory || s.category === selectedCategory)
.map((screen) => {
const catColors = COLORS[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
return (
<button
key={screen.id}
onClick={() => {
setSelectedNode(screen.id)
setSelectedCategory(null)
setPreviewScreen(screen)
}}
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 transition-colors text-left"
>
<span
className="w-9 h-9 rounded-lg flex items-center justify-center text-lg"
style={{ background: catColors.bg }}
>
{screen.icon}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-slate-800 text-sm">{screen.name}</div>
<div className="text-xs text-slate-500 truncate">{screen.description}</div>
</div>
<span
className="px-2 py-1 rounded text-xs font-medium shrink-0"
style={{ background: catColors.bg, color: catColors.text }}
>
{LABELS[screen.category]}
</span>
</button>
)
})}
</div>
</div>
</div>
)
}