[split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ColorShade } from '../constants'
|
||||
|
||||
export function ColorSwatch({ color }: { color: ColorShade }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(color.value)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="group flex flex-col items-center"
|
||||
title={`Klicken zum Kopieren: ${color.value}`}
|
||||
>
|
||||
<div
|
||||
className="w-16 h-16 rounded-xl shadow-sm border border-slate-200 transition-transform group-hover:scale-110 flex items-center justify-center"
|
||||
style={{ backgroundColor: color.value }}
|
||||
>
|
||||
{copied && (
|
||||
<svg className={`w-5 h-5 ${color.text === 'light' ? 'text-white' : 'text-slate-900'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 mt-1 font-medium">{color.name}</span>
|
||||
<span className="text-xs text-slate-400 font-mono">{color.value}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
PRIMARY_COLORS,
|
||||
ACCENT_COLORS,
|
||||
CATEGORY_COLORS,
|
||||
SEMANTIC_COLORS,
|
||||
} from '../constants'
|
||||
import { ColorSwatch } from './ColorSwatch'
|
||||
|
||||
export function ColorsTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Primary Color */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">{PRIMARY_COLORS.name}</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">{PRIMARY_COLORS.description}</p>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{PRIMARY_COLORS.shades.map((shade) => (
|
||||
<ColorSwatch key={shade.name} color={shade} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accent Color */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">{ACCENT_COLORS.name}</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">{ACCENT_COLORS.description}</p>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{ACCENT_COLORS.shades.map((shade) => (
|
||||
<ColorSwatch key={shade.name} color={shade} />
|
||||
))}
|
||||
</div>
|
||||
{/* Logo Gradient Preview */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-200">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Logo-Gradient</h4>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-2xl font-bold text-white">B</span>
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
<code className="bg-slate-100 px-2 py-1 rounded font-mono text-xs">
|
||||
bg-gradient-to-br from-primary-500 to-accent-500
|
||||
</code>
|
||||
<p className="mt-1 text-slate-500">Sky Blue (#0ea5e9) → Fuchsia (#d946ef)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Colors */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategorie-Farben</h3>
|
||||
<p className="text-sm text-slate-500 mb-6">
|
||||
Jede Kategorie hat eine eigene Farbe fuer Navigation, Module und Akzente.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{CATEGORY_COLORS.map((cat) => (
|
||||
<div key={cat.id} className="border border-slate-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg"
|
||||
style={{ backgroundColor: cat.main }}
|
||||
/>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900">{cat.name}</h4>
|
||||
<p className="text-xs text-slate-500">{cat.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{cat.shades.map((shade) => (
|
||||
<ColorSwatch key={shade.name} color={shade} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Semantic Colors */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Semantische Farben</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{Object.entries(SEMANTIC_COLORS).map(([key, palette]) => (
|
||||
<div key={key} className="border border-slate-200 rounded-xl p-4">
|
||||
<h4 className="font-medium text-slate-900 mb-3">{palette.name}</h4>
|
||||
<div className="flex gap-2">
|
||||
{palette.shades.map((shade) => (
|
||||
<ColorSwatch key={shade.name} color={shade} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Guidelines */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Verwendungsrichtlinien</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-700 mb-2">Primary (Sky Blue)</h4>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- Primaere Buttons und CTAs</li>
|
||||
<li>- Links und interaktive Elemente</li>
|
||||
<li>- Fokuszustaende</li>
|
||||
<li>- Ausgewaehlte Navigation</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-700 mb-2">Kategorie-Farben</h4>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- Sidebar-Navigation Icons</li>
|
||||
<li>- Modul-Cards und Badges</li>
|
||||
<li>- Bereichs-spezifische Akzente</li>
|
||||
<li>- Breadcrumbs und Headers</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import { CATEGORY_COLORS } from '../constants'
|
||||
|
||||
export function ComponentsTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Buttons */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Buttons</h3>
|
||||
<div className="flex flex-wrap gap-4 mb-6">
|
||||
<button className="px-6 py-2 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 transition-colors">
|
||||
Primary
|
||||
</button>
|
||||
<button className="px-6 py-2 bg-slate-100 text-slate-700 font-medium rounded-lg hover:bg-slate-200 transition-colors">
|
||||
Secondary
|
||||
</button>
|
||||
<button className="px-6 py-2 border border-slate-300 text-slate-700 font-medium rounded-lg hover:bg-slate-50 transition-colors">
|
||||
Outline
|
||||
</button>
|
||||
<button className="px-6 py-2 text-primary-600 font-medium rounded-lg hover:bg-primary-50 transition-colors">
|
||||
Ghost
|
||||
</button>
|
||||
<button className="px-6 py-2 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 transition-colors">
|
||||
Danger
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button className="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg">
|
||||
Small
|
||||
</button>
|
||||
<button className="px-6 py-2 bg-primary-600 text-white font-medium rounded-lg">
|
||||
Medium
|
||||
</button>
|
||||
<button className="px-8 py-3 bg-primary-600 text-white text-lg font-medium rounded-lg">
|
||||
Large
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inputs */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Eingabefelder</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Standard Input</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Placeholder..."
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Mit Icon</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
className="w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none"
|
||||
/>
|
||||
<svg className="w-5 h-5 text-slate-400 absolute left-3 top-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Cards</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 hover:shadow-lg hover:border-primary-300 transition-all cursor-pointer">
|
||||
<div className="w-10 h-10 bg-primary-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-5 h-5 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-1">Feature Card</h4>
|
||||
<p className="text-sm text-slate-600">Standard Card mit Hover-Effekt.</p>
|
||||
</div>
|
||||
<div className="rounded-xl p-4 text-white" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<h4 className="font-semibold mb-1">Highlight Card</h4>
|
||||
<p className="text-sm text-white/80">Mit Gradient-Hintergrund.</p>
|
||||
</div>
|
||||
<div className="bg-slate-900 rounded-xl p-4 text-white">
|
||||
<h4 className="font-semibold mb-1">Dark Card</h4>
|
||||
<p className="text-sm text-slate-400">Sidebar-Style.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Cards */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategorie-Cards</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{CATEGORY_COLORS.slice(0, 3).map((cat) => (
|
||||
<div
|
||||
key={cat.id}
|
||||
className="rounded-xl border p-4 hover:shadow-lg transition-all cursor-pointer"
|
||||
style={{
|
||||
borderColor: cat.shades[0].value,
|
||||
backgroundColor: cat.shades[0].value + '40'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center mb-3"
|
||||
style={{ backgroundColor: cat.main }}
|
||||
>
|
||||
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-1">{cat.name.split(' (')[0]}</h4>
|
||||
<p className="text-sm text-slate-600">{cat.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Badges & Tags</h3>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<span className="px-2.5 py-1 bg-primary-100 text-primary-700 text-xs font-medium rounded-full">Primary</span>
|
||||
<span className="px-2.5 py-1 bg-green-100 text-green-700 text-xs font-medium rounded-full">Success</span>
|
||||
<span className="px-2.5 py-1 bg-amber-100 text-amber-700 text-xs font-medium rounded-full">Warning</span>
|
||||
<span className="px-2.5 py-1 bg-red-100 text-red-700 text-xs font-medium rounded-full">Danger</span>
|
||||
<span className="px-2.5 py-1 bg-slate-100 text-slate-700 text-xs font-medium rounded-full">Neutral</span>
|
||||
</div>
|
||||
<h4 className="font-medium text-slate-700 mb-2 mt-4">Kategorie-Badges</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATEGORY_COLORS.map((cat) => (
|
||||
<span
|
||||
key={cat.id}
|
||||
className="px-2.5 py-1 text-xs font-medium rounded-full"
|
||||
style={{
|
||||
backgroundColor: cat.shades[0].value,
|
||||
color: cat.shades[3].value
|
||||
}}
|
||||
>
|
||||
{cat.name.split(' (')[0]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Border Radius */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Border Radius</h3>
|
||||
<div className="flex gap-6 items-end">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary-500 rounded"></div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">rounded (4px)</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary-500 rounded-lg"></div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">rounded-lg (8px)</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary-500 rounded-xl"></div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">rounded-xl (12px)</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary-500 rounded-2xl"></div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">rounded-2xl (16px)</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary-500 rounded-full"></div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">rounded-full</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
'use client'
|
||||
|
||||
function LogoDisplay() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Logo</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-2xl font-bold text-white">B</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl font-bold text-slate-900">BreakPilot</span>
|
||||
<span className="text-sm text-slate-500 block">Admin v2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-900 rounded-xl p-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #38bdf8, #e879f9)' }}>
|
||||
<span className="text-2xl font-bold text-white">B</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl font-bold text-white">BreakPilot</span>
|
||||
<span className="text-sm text-slate-400 block">Admin v2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Gradient Info */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<code className="bg-slate-100 px-3 py-1.5 rounded-lg font-mono text-xs text-slate-700">
|
||||
background: linear-gradient(to bottom right, #0ea5e9, #d946ef)
|
||||
</code>
|
||||
<span className="text-sm text-slate-500">Primary → Accent (Sky Blue → Fuchsia)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LogoVariations() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Logo-Varianten</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="border border-slate-200 rounded-xl p-4 flex flex-col items-center gap-2">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-2xl font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">Icon Only</span>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-xl p-4 flex flex-col items-center gap-2">
|
||||
<span className="text-xl font-bold text-slate-900">BreakPilot</span>
|
||||
<span className="text-xs text-slate-600">Text Only</span>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-xl p-4 flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-lg font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-slate-900">BreakPilot</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">Horizontal</span>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-xl p-4 flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center mb-1" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-xl font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-900">BreakPilot</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">Stacked</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FaviconSizes() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Favicon & App Icon</h3>
|
||||
<div className="flex gap-6 items-end">
|
||||
{[
|
||||
{ size: 'w-8 h-8', rounded: 'rounded', text: 'text-sm', label: '16x16' },
|
||||
{ size: 'w-12 h-12', rounded: 'rounded-lg', text: 'text-xl', label: '32x32' },
|
||||
{ size: 'w-16 h-16', rounded: 'rounded-xl', text: 'text-2xl', label: '180x180' },
|
||||
{ size: 'w-24 h-24', rounded: 'rounded-2xl', text: 'text-4xl', label: '512x512' },
|
||||
].map((icon) => (
|
||||
<div key={icon.label} className="text-center">
|
||||
<div className={`${icon.size} ${icon.rounded} flex items-center justify-center`} style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className={`${icon.text} font-bold text-white`}>B</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 mt-2 block">{icon.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LogoDonts() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Nicht erlaubt</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="border border-red-200 bg-red-50 rounded-xl p-4 text-center">
|
||||
<div className="flex items-center gap-2 justify-center opacity-50">
|
||||
<div className="w-8 h-8 bg-red-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-lg font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-red-500">BreakPilot</span>
|
||||
</div>
|
||||
<span className="text-xs text-red-600 mt-2 block">Falsche Farben</span>
|
||||
</div>
|
||||
<div className="border border-red-200 bg-red-50 rounded-xl p-4 text-center">
|
||||
<div className="flex items-center gap-2 justify-center" style={{ transform: 'skewX(-10deg)' }}>
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-lg font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-slate-900">BreakPilot</span>
|
||||
</div>
|
||||
<span className="text-xs text-red-600 mt-2 block">Verzerrt</span>
|
||||
</div>
|
||||
<div className="border border-red-200 bg-red-50 rounded-xl p-4 text-center">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(to bottom right, #0ea5e9, #d946ef)' }}>
|
||||
<span className="text-lg font-bold text-white">B</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-slate-900">Break Pilot</span>
|
||||
</div>
|
||||
<span className="text-xs text-red-600 mt-2 block">Falsche Schreibweise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StudioLogo() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">Studio Logo (BP mit Fluegeln)</h3>
|
||||
<p className="text-sm text-slate-500 mb-6">
|
||||
Das Studio-Logo verwendet ein rotes Farbschema und zeigt "BP" mit stilisierten Fluegeln.
|
||||
Es gibt drei Design-Varianten fuer unterschiedliche Anwendungsfaelle.
|
||||
</p>
|
||||
|
||||
{/* Variante A: Cupertino Clean */}
|
||||
<div className="mb-6 p-4 bg-slate-50 rounded-xl">
|
||||
<h4 className="font-medium text-slate-700 mb-4">A: Cupertino Clean</h4>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 relative">
|
||||
<div className="absolute inset-0 rounded-xl shadow-lg" style={{ background: 'linear-gradient(to bottom right, #ef4444, #b91c1c)' }} />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg viewBox="0 0 40 40" className="w-9 h-9">
|
||||
<circle cx="20" cy="20" r="14" fill="rgba(255,255,255,0.15)" />
|
||||
<text x="20" y="26" textAnchor="middle" fill="white" fontSize="16" fontWeight="bold" fontFamily="system-ui">BP</text>
|
||||
<path d="M6 22 L12 20 M28 20 L34 22" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-slate-900 text-lg">Break<span className="text-red-600">Pilot</span></span>
|
||||
<span className="text-slate-400 text-sm block">Studio</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
<code className="bg-white px-2 py-1 rounded font-mono text-xs">#ef4444 → #b91c1c</code>
|
||||
<p className="mt-1 text-slate-500">Minimalistisch, SF-Style, BP mit Fluegel-Linien</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variante B: Glassmorphism Pro */}
|
||||
<div className="mb-6 p-4 rounded-xl" style={{ background: 'linear-gradient(to bottom right, #312e81, #581c87, #831843)' }}>
|
||||
<h4 className="font-medium text-white/80 mb-4">B: Glassmorphism Pro</h4>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 relative">
|
||||
<div className="absolute inset-0 rounded-2xl blur-md opacity-50" style={{ background: 'linear-gradient(to bottom right, #f87171, #dc2626)' }} />
|
||||
<div className="absolute inset-0 rounded-2xl border border-white/20 shadow-xl" style={{ background: 'linear-gradient(to bottom right, rgba(248,113,113,0.9), rgba(220,38,38,0.9))' }} />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg viewBox="0 0 40 40" className="w-9 h-9">
|
||||
<path d="M10 28 L20 8 L30 28 L20 22 Z" fill="rgba(255,255,255,0.9)" />
|
||||
<path d="M14 24 L20 12 L26 24 L20 20 Z" fill="rgba(255,255,255,0.3)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-white text-lg">BreakPilot</span>
|
||||
<span className="text-white/60 text-sm block">Studio</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
<code className="bg-white/10 px-2 py-1 rounded font-mono text-xs">#f87171 → #dc2626</code>
|
||||
<p className="mt-1 text-white/50">Glassmorphism, Glow-Effekt, Papierflugzeug-Silhouette</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variante C: Bento Style */}
|
||||
<div className="mb-6 p-4 bg-black rounded-xl">
|
||||
<h4 className="font-medium text-white/80 mb-4">C: Bento Style</h4>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 relative">
|
||||
<div className="absolute inset-0 rounded-xl" style={{ background: 'linear-gradient(to bottom right, #ef4444, #991b1b)' }} />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg viewBox="0 0 40 40" className="w-9 h-9">
|
||||
<circle cx="15" cy="14" r="5" stroke="white" strokeWidth="1.5" fill="none" />
|
||||
<circle cx="15" cy="24" r="5" stroke="white" strokeWidth="1.5" fill="none" />
|
||||
<line x1="27" y1="10" x2="27" y2="30" stroke="white" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<circle cx="27" cy="15" r="4" stroke="white" strokeWidth="1.5" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-white text-lg">BreakPilot</span>
|
||||
<span className="text-white/40 text-sm ml-2">Studio</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
<code className="bg-white/10 px-2 py-1 rounded font-mono text-xs">#ef4444 → #991b1b</code>
|
||||
<p className="mt-1 text-white/50">Geometrisch, Monoweight, B+P als Kreise/Linien</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Studio Farbpalette */}
|
||||
<div className="pt-4 border-t border-slate-200">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Studio Farbpalette (Rot)</h4>
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
{ bg: '#fef2f2', label: 'red-50' },
|
||||
{ bg: '#f87171', label: 'red-400' },
|
||||
{ bg: '#ef4444', label: 'red-500' },
|
||||
{ bg: '#dc2626', label: 'red-600' },
|
||||
{ bg: '#b91c1c', label: 'red-700' },
|
||||
{ bg: '#991b1b', label: 'red-800' },
|
||||
].map((swatch) => (
|
||||
<div key={swatch.label} className="text-center">
|
||||
<div className="w-12 h-12 rounded-lg" style={{ backgroundColor: swatch.bg }} />
|
||||
<span className="text-xs text-slate-500 mt-1 block">{swatch.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-3">
|
||||
Quelle: <code className="bg-slate-100 px-1.5 py-0.5 rounded">/studio-v2/components/Logo.tsx</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LogoTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<LogoDisplay />
|
||||
<LogoVariations />
|
||||
<FaviconSizes />
|
||||
<LogoDonts />
|
||||
<StudioLogo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
'use client'
|
||||
|
||||
import { TYPOGRAPHY, SPACING } from '../constants'
|
||||
|
||||
export function TypographyTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Font Family */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schriftart: Inter</h3>
|
||||
<div className="bg-slate-50 rounded-lg p-4 font-mono text-sm text-slate-600 mb-4">
|
||||
font-family: {TYPOGRAPHY.fontFamily};
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">
|
||||
Inter ist eine moderne, variable Sans-Serif Schrift, optimiert fuer Bildschirme.
|
||||
Sie ist unter der SIL Open Font License verfuegbar und frei fuer kommerzielle Nutzung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Font Licenses */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Lizenzen</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Inter License */}
|
||||
<div className="border border-slate-200 rounded-xl p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-900">Inter Font</h4>
|
||||
<p className="text-sm text-slate-500">Designer: Rasmus Andersson</p>
|
||||
</div>
|
||||
<span className="px-2.5 py-1 bg-green-100 text-green-700 text-xs font-medium rounded-full">
|
||||
OFL-1.1
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3 text-sm">
|
||||
<div>
|
||||
<span className="text-slate-500">Lizenz:</span>
|
||||
<span className="ml-2 text-slate-700">SIL Open Font License 1.1</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Kommerzielle Nutzung:</span>
|
||||
<span className="ml-2 text-green-600 font-medium">Ja, uneingeschraenkt</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Attribution erforderlich:</span>
|
||||
<span className="ml-2 text-slate-700">Nein</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Repository:</span>
|
||||
<a href="https://github.com/rsms/inter" target="_blank" rel="noopener noreferrer" className="ml-2 text-primary-600 hover:underline">
|
||||
github.com/rsms/inter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Heroicons License */}
|
||||
<div className="border border-slate-200 rounded-xl p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-900">Heroicons</h4>
|
||||
<p className="text-sm text-slate-500">Designer: Tailwind Labs</p>
|
||||
</div>
|
||||
<span className="px-2.5 py-1 bg-blue-100 text-blue-700 text-xs font-medium rounded-full">
|
||||
MIT
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3 text-sm">
|
||||
<div>
|
||||
<span className="text-slate-500">Lizenz:</span>
|
||||
<span className="ml-2 text-slate-700">MIT License</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Kommerzielle Nutzung:</span>
|
||||
<span className="ml-2 text-green-600 font-medium">Ja, uneingeschraenkt</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Attribution erforderlich:</span>
|
||||
<span className="ml-2 text-slate-700">Nein (empfohlen)</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Repository:</span>
|
||||
<a href="https://github.com/tailwindlabs/heroicons" target="_blank" rel="noopener noreferrer" className="ml-2 text-primary-600 hover:underline">
|
||||
github.com/tailwindlabs/heroicons
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* License Summary */}
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h4 className="font-semibold text-green-800">Lizenz-Compliance</h4>
|
||||
</div>
|
||||
<p className="text-sm text-green-700">
|
||||
Alle verwendeten Schriftarten und Icons sind Open Source und fuer kommerzielle Nutzung freigegeben.
|
||||
Keine Attribution erforderlich. Vollstaendige Dokumentation in der SBOM.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font Weights */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schriftschnitte</h3>
|
||||
<div className="space-y-4">
|
||||
{TYPOGRAPHY.weights.map((w) => (
|
||||
<div key={w.weight} className="flex items-center gap-6 pb-4 border-b border-slate-100 last:border-0 last:pb-0">
|
||||
<span
|
||||
className="text-2xl w-64"
|
||||
style={{ fontWeight: w.weight }}
|
||||
>
|
||||
The quick brown fox
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-slate-900">{w.name} ({w.weight})</span>
|
||||
<span className="text-sm text-slate-500 ml-4">{w.usage}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font Sizes */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schriftgroessen</h3>
|
||||
<div className="space-y-3">
|
||||
{TYPOGRAPHY.sizes.map((s) => (
|
||||
<div key={s.name} className="flex items-baseline gap-4 border-b border-slate-100 pb-3 last:border-0">
|
||||
<span className="w-16 text-sm font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded">{s.name}</span>
|
||||
<span className="w-36 text-sm text-slate-600">{s.size}</span>
|
||||
<span className="text-sm text-slate-500">{s.usage}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headings Preview */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ueberschriften-Hierarchie</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="text-4xl font-bold text-slate-900">H1: Hauptueberschrift</div>
|
||||
<div className="text-3xl font-bold text-slate-900">H2: Abschnittsueberschrift</div>
|
||||
<div className="text-2xl font-semibold text-slate-900">H3: Unterabschnitt</div>
|
||||
<div className="text-xl font-semibold text-slate-900">H4: Card-Titel</div>
|
||||
<div className="text-lg font-medium text-slate-900">H5: Kleiner Titel</div>
|
||||
<div className="text-base font-medium text-slate-900">H6: Label</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spacing */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Spacing Scale</h3>
|
||||
<div className="space-y-3">
|
||||
{SPACING.map((s) => (
|
||||
<div key={s.name} className="flex items-center gap-4">
|
||||
<span className="w-12 text-sm font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded text-center">{s.name}</span>
|
||||
<div
|
||||
className="bg-primary-500 h-4 rounded"
|
||||
style={{ width: `${parseInt(s.value) * 16}px` }}
|
||||
/>
|
||||
<span className="text-sm text-slate-600">{s.value}</span>
|
||||
<span className="text-sm text-slate-400">{s.usage}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { VOICE_TONE } from '../constants'
|
||||
|
||||
export function VoiceTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Brand Attributes */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Markenpersoenlichkeit</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{VOICE_TONE.attributes.map((attr) => (
|
||||
<span
|
||||
key={attr}
|
||||
className="px-4 py-2 bg-primary-100 text-primary-700 rounded-full font-medium"
|
||||
>
|
||||
{attr}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Do & Dont */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-green-600 mb-4 flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
So schreiben wir
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{VOICE_TONE.doList.map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-slate-600">
|
||||
<span className="w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-red-600 mb-4 flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Das vermeiden wir
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{VOICE_TONE.dontList.map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-slate-600">
|
||||
<span className="w-1.5 h-1.5 bg-red-500 rounded-full flex-shrink-0"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Example Texts */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Beispieltexte</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-xl">
|
||||
<span className="text-xs text-green-600 font-medium mb-2 block">GUT</span>
|
||||
<p className="text-slate-700">
|
||||
"Verwalten Sie Ihre Datenschutz-Dokumente einfach und sicher. Alle Aenderungen werden protokolliert."
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-xl">
|
||||
<span className="text-xs text-red-600 font-medium mb-2 block">SCHLECHT</span>
|
||||
<p className="text-slate-700">
|
||||
"Unsere revolutionaere KI-Loesung optimiert Ihre Compliance-Workflows durch state-of-the-art NLP."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Audience */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Zielgruppe</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{
|
||||
iconBg: 'bg-education-100', iconColor: 'text-education-600',
|
||||
iconPath: 'M12 14l9-5-9-5-9 5 9 5z',
|
||||
iconPath2: 'M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z',
|
||||
title: 'Lehrkraefte', desc: 'Zeitersparnis, einfache Bedienung',
|
||||
},
|
||||
{
|
||||
iconBg: 'bg-compliance-100', iconColor: 'text-compliance-600',
|
||||
iconPath: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
title: 'DSB', desc: 'DSGVO-Compliance, Dokumentation',
|
||||
},
|
||||
{
|
||||
iconBg: 'bg-infrastructure-100', iconColor: 'text-infrastructure-600',
|
||||
iconPath: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
|
||||
title: 'Entwickler', desc: 'APIs, Integration, DevOps',
|
||||
},
|
||||
{
|
||||
iconBg: 'bg-communication-100', iconColor: 'text-communication-600',
|
||||
iconPath: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
|
||||
title: 'Schulleitung', desc: 'Ueberblick, Kosten, Compliance',
|
||||
},
|
||||
].map((audience) => (
|
||||
<div key={audience.title} className="p-4 bg-slate-50 rounded-xl text-center">
|
||||
<div className={`w-12 h-12 ${audience.iconBg} rounded-xl flex items-center justify-center mx-auto mb-2`}>
|
||||
<svg className={`w-6 h-6 ${audience.iconColor}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={audience.iconPath} />
|
||||
{audience.iconPath2 && (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={audience.iconPath2} />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900">{audience.title}</h4>
|
||||
<p className="text-sm text-slate-600 mt-1">{audience.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
253
admin-lehrer/app/(admin)/development/brandbook/constants.ts
Normal file
253
admin-lehrer/app/(admin)/development/brandbook/constants.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Brandbook Constants - Color palettes, typography, spacing, voice & tone
|
||||
*/
|
||||
|
||||
export interface ColorShade {
|
||||
name: string
|
||||
value: string
|
||||
text: 'dark' | 'light'
|
||||
}
|
||||
|
||||
export interface ColorPalette {
|
||||
name: string
|
||||
description: string
|
||||
shades: ColorShade[]
|
||||
}
|
||||
|
||||
export interface CategoryColor {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
main: string
|
||||
shades: ColorShade[]
|
||||
}
|
||||
|
||||
// Primary brand color (Sky Blue)
|
||||
export const PRIMARY_COLORS: ColorPalette = {
|
||||
name: 'Primary (Sky Blue)',
|
||||
description: 'Haupt-Markenfarbe fuer Buttons, Links und Akzente',
|
||||
shades: [
|
||||
{ name: '50', value: '#f0f9ff', text: 'dark' },
|
||||
{ name: '100', value: '#e0f2fe', text: 'dark' },
|
||||
{ name: '200', value: '#bae6fd', text: 'dark' },
|
||||
{ name: '300', value: '#7dd3fc', text: 'dark' },
|
||||
{ name: '400', value: '#38bdf8', text: 'dark' },
|
||||
{ name: '500', value: '#0ea5e9', text: 'light' },
|
||||
{ name: '600', value: '#0284c7', text: 'light' },
|
||||
{ name: '700', value: '#0369a1', text: 'light' },
|
||||
{ name: '800', value: '#075985', text: 'light' },
|
||||
{ name: '900', value: '#0c4a6e', text: 'light' },
|
||||
],
|
||||
}
|
||||
|
||||
// Accent brand color (Fuchsia) - for logo gradient and highlights
|
||||
export const ACCENT_COLORS: ColorPalette = {
|
||||
name: 'Accent (Fuchsia)',
|
||||
description: 'Akzentfarbe fuer Logo-Gradient und besondere Highlights',
|
||||
shades: [
|
||||
{ name: '50', value: '#fdf4ff', text: 'dark' },
|
||||
{ name: '100', value: '#fae8ff', text: 'dark' },
|
||||
{ name: '200', value: '#f5d0fe', text: 'dark' },
|
||||
{ name: '300', value: '#f0abfc', text: 'dark' },
|
||||
{ name: '400', value: '#e879f9', text: 'dark' },
|
||||
{ name: '500', value: '#d946ef', text: 'light' },
|
||||
{ name: '600', value: '#c026d3', text: 'light' },
|
||||
{ name: '700', value: '#a21caf', text: 'light' },
|
||||
{ name: '800', value: '#86198f', text: 'light' },
|
||||
{ name: '900', value: '#701a75', text: 'light' },
|
||||
],
|
||||
}
|
||||
|
||||
// Category colors for navigation and modules
|
||||
export const CATEGORY_COLORS: CategoryColor[] = [
|
||||
{
|
||||
id: 'dsgvo',
|
||||
name: 'DSGVO (Violet)',
|
||||
description: 'Datenschutz & Betroffenenrechte',
|
||||
main: '#7c3aed',
|
||||
shades: [
|
||||
{ name: '100', value: '#ede9fe', text: 'dark' },
|
||||
{ name: '500', value: '#8b5cf6', text: 'light' },
|
||||
{ name: '600', value: '#7c3aed', text: 'light' },
|
||||
{ name: '700', value: '#6d28d9', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'compliance',
|
||||
name: 'Compliance (Purple)',
|
||||
description: 'Audit, Controls & Regulierung',
|
||||
main: '#9333ea',
|
||||
shades: [
|
||||
{ name: '100', value: '#f3e8ff', text: 'dark' },
|
||||
{ name: '500', value: '#a855f7', text: 'light' },
|
||||
{ name: '600', value: '#9333ea', text: 'light' },
|
||||
{ name: '700', value: '#7e22ce', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
name: 'KI & Automatisierung (Teal)',
|
||||
description: 'LLM, OCR, RAG & Machine Learning',
|
||||
main: '#14b8a6',
|
||||
shades: [
|
||||
{ name: '100', value: '#ccfbf1', text: 'dark' },
|
||||
{ name: '500', value: '#14b8a6', text: 'light' },
|
||||
{ name: '600', value: '#0d9488', text: 'light' },
|
||||
{ name: '700', value: '#0f766e', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'infrastructure',
|
||||
name: 'Infrastruktur (Orange)',
|
||||
description: 'GPU, Security, CI/CD & Monitoring',
|
||||
main: '#f97316',
|
||||
shades: [
|
||||
{ name: '100', value: '#ffedd5', text: 'dark' },
|
||||
{ name: '500', value: '#f97316', text: 'light' },
|
||||
{ name: '600', value: '#ea580c', text: 'light' },
|
||||
{ name: '700', value: '#c2410c', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
name: 'Bildung & Schule (Blue)',
|
||||
description: 'Bildungsquellen & Lehrplaene',
|
||||
main: '#3b82f6',
|
||||
shades: [
|
||||
{ name: '100', value: '#dbeafe', text: 'dark' },
|
||||
{ name: '500', value: '#3b82f6', text: 'light' },
|
||||
{ name: '600', value: '#2563eb', text: 'light' },
|
||||
{ name: '700', value: '#1d4ed8', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'communication',
|
||||
name: 'Kommunikation (Green)',
|
||||
description: 'Matrix, E-Mail & Benachrichtigungen',
|
||||
main: '#22c55e',
|
||||
shades: [
|
||||
{ name: '100', value: '#dcfce7', text: 'dark' },
|
||||
{ name: '500', value: '#22c55e', text: 'light' },
|
||||
{ name: '600', value: '#16a34a', text: 'light' },
|
||||
{ name: '700', value: '#15803d', text: 'light' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'development',
|
||||
name: 'Entwicklung (Slate)',
|
||||
description: 'Workflow, Game, Docs & Brandbook',
|
||||
main: '#64748b',
|
||||
shades: [
|
||||
{ name: '100', value: '#f1f5f9', text: 'dark' },
|
||||
{ name: '500', value: '#64748b', text: 'light' },
|
||||
{ name: '600', value: '#475569', text: 'light' },
|
||||
{ name: '700', value: '#334155', text: 'light' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Semantic colors
|
||||
export const SEMANTIC_COLORS: Record<string, { name: string; shades: ColorShade[] }> = {
|
||||
success: {
|
||||
name: 'Success (Emerald)',
|
||||
shades: [
|
||||
{ name: '100', value: '#d1fae5', text: 'dark' },
|
||||
{ name: '500', value: '#10b981', text: 'light' },
|
||||
{ name: '600', value: '#059669', text: 'light' },
|
||||
],
|
||||
},
|
||||
warning: {
|
||||
name: 'Warning (Amber)',
|
||||
shades: [
|
||||
{ name: '100', value: '#fef3c7', text: 'dark' },
|
||||
{ name: '500', value: '#f59e0b', text: 'dark' },
|
||||
{ name: '600', value: '#d97706', text: 'light' },
|
||||
],
|
||||
},
|
||||
danger: {
|
||||
name: 'Danger (Red)',
|
||||
shades: [
|
||||
{ name: '100', value: '#fee2e2', text: 'dark' },
|
||||
{ name: '500', value: '#ef4444', text: 'light' },
|
||||
{ name: '600', value: '#dc2626', text: 'light' },
|
||||
],
|
||||
},
|
||||
neutral: {
|
||||
name: 'Neutral (Slate)',
|
||||
shades: [
|
||||
{ name: '50', value: '#f8fafc', text: 'dark' },
|
||||
{ name: '100', value: '#f1f5f9', text: 'dark' },
|
||||
{ name: '200', value: '#e2e8f0', text: 'dark' },
|
||||
{ name: '300', value: '#cbd5e1', text: 'dark' },
|
||||
{ name: '400', value: '#94a3b8', text: 'dark' },
|
||||
{ name: '500', value: '#64748b', text: 'light' },
|
||||
{ name: '600', value: '#475569', text: 'light' },
|
||||
{ name: '700', value: '#334155', text: 'light' },
|
||||
{ name: '800', value: '#1e293b', text: 'light' },
|
||||
{ name: '900', value: '#0f172a', text: 'light' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const TYPOGRAPHY = {
|
||||
fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
|
||||
weights: [
|
||||
{ weight: 400, name: 'Regular', usage: 'Fliesstext, Beschreibungen' },
|
||||
{ weight: 500, name: 'Medium', usage: 'Labels, Buttons, Navigation' },
|
||||
{ weight: 600, name: 'Semi-Bold', usage: 'Ueberschriften H3-H6, Card-Titel' },
|
||||
{ weight: 700, name: 'Bold', usage: 'Ueberschriften H1-H2, CTAs' },
|
||||
],
|
||||
sizes: [
|
||||
{ name: 'xs', size: '0.75rem (12px)', usage: 'Footnotes, Badges' },
|
||||
{ name: 'sm', size: '0.875rem (14px)', usage: 'Nebentext, Labels' },
|
||||
{ name: 'base', size: '1rem (16px)', usage: 'Fliesstext, Body' },
|
||||
{ name: 'lg', size: '1.125rem (18px)', usage: 'Lead Text' },
|
||||
{ name: 'xl', size: '1.25rem (20px)', usage: 'H4, Card Titles' },
|
||||
{ name: '2xl', size: '1.5rem (24px)', usage: 'H3' },
|
||||
{ name: '3xl', size: '1.875rem (30px)', usage: 'H2' },
|
||||
{ name: '4xl', size: '2.25rem (36px)', usage: 'H1, Hero' },
|
||||
],
|
||||
}
|
||||
|
||||
export const SPACING = [
|
||||
{ name: '1', value: '0.25rem (4px)', usage: 'Minimaler Abstand' },
|
||||
{ name: '2', value: '0.5rem (8px)', usage: 'Kompakter Abstand' },
|
||||
{ name: '3', value: '0.75rem (12px)', usage: 'Standard klein' },
|
||||
{ name: '4', value: '1rem (16px)', usage: 'Standard' },
|
||||
{ name: '6', value: '1.5rem (24px)', usage: 'Card Padding' },
|
||||
{ name: '8', value: '2rem (32px)', usage: 'Section Spacing' },
|
||||
]
|
||||
|
||||
export const VOICE_TONE = {
|
||||
attributes: [
|
||||
'Professionell & Vertrauenswuerdig',
|
||||
'Klar & Direkt',
|
||||
'Hilfreich & Unterstuetzend',
|
||||
'Modern & Innovativ',
|
||||
'DSGVO-konform & Sicher',
|
||||
],
|
||||
doList: [
|
||||
'Einfache, klare Sprache verwenden',
|
||||
'Aktiv formulieren',
|
||||
'Nutzenorientiert schreiben',
|
||||
'Konkrete Beispiele geben',
|
||||
'Technische Begriffe erklaeren',
|
||||
],
|
||||
dontList: [
|
||||
'Fachjargon ohne Erklaerung',
|
||||
'Passive Formulierungen',
|
||||
'Uebertriebene Versprechen',
|
||||
'Marketing-Floskeln',
|
||||
'Unpersoenliche Ansprache',
|
||||
],
|
||||
}
|
||||
|
||||
export type BrandbookTab = 'colors' | 'typography' | 'components' | 'logo' | 'voice'
|
||||
|
||||
export const TABS: { id: BrandbookTab; label: string }[] = [
|
||||
{ id: 'colors', label: 'Farben' },
|
||||
{ id: 'typography', label: 'Typografie' },
|
||||
{ id: 'components', label: 'Komponenten' },
|
||||
{ id: 'logo', label: 'Logo' },
|
||||
{ id: 'voice', label: 'Tonalitaet' },
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import type { ScreenDefinition } from '../types'
|
||||
|
||||
interface CategoryFilterProps {
|
||||
screens: ScreenDefinition[]
|
||||
categories: string[]
|
||||
colors: Record<string, { bg: string; border: string; text: string }>
|
||||
labels: Record<string, string>
|
||||
selectedCategory: string | null
|
||||
selectedNode: string | null
|
||||
onSelectCategory: (category: string | null) => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
export function CategoryFilter({
|
||||
screens,
|
||||
categories,
|
||||
colors,
|
||||
labels,
|
||||
selectedCategory,
|
||||
selectedNode,
|
||||
onSelectCategory,
|
||||
onReset,
|
||||
}: CategoryFilterProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={onReset}
|
||||
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={() => onSelectCategory(selectedCategory === key ? null : key)}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import type { ScreenDefinition } from '../types'
|
||||
|
||||
interface ConnectedScreensListProps {
|
||||
selectedNode: string | null
|
||||
connectedScreens: ScreenDefinition[]
|
||||
colors: Record<string, { bg: string; border: string; text: string }>
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
export function ConnectedScreensList({
|
||||
selectedNode,
|
||||
connectedScreens,
|
||||
colors,
|
||||
baseUrl,
|
||||
}: ConnectedScreensListProps) {
|
||||
if (!selectedNode || connectedScreens.length <= 1) return null
|
||||
|
||||
return (
|
||||
<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-primary-500' : ''
|
||||
}`}
|
||||
style={{
|
||||
background: isCurrentNode ? catColors.border : catColors.bg,
|
||||
color: isCurrentNode ? 'white' : catColors.text,
|
||||
}}
|
||||
>
|
||||
<span>{screen.icon}</span>
|
||||
{screen.name}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
Edge,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
BackgroundVariant,
|
||||
Panel,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
import type { ScreenDefinition, FlowType } from '../types'
|
||||
|
||||
interface FlowDiagramProps {
|
||||
flowType: FlowType
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
onNodesChange: (changes: NodeChange[]) => void
|
||||
onEdgesChange: (changes: EdgeChange[]) => void
|
||||
onNodeClick: (event: React.MouseEvent, node: Node) => void
|
||||
onPaneClick: () => void
|
||||
screens: ScreenDefinition[]
|
||||
colors: Record<string, { bg: string; border: string; text: string }>
|
||||
labels: Record<string, string>
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export function FlowDiagram({
|
||||
flowType,
|
||||
nodes,
|
||||
edges,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
screens,
|
||||
colors,
|
||||
labels,
|
||||
categories,
|
||||
}: FlowDiagramProps) {
|
||||
return (
|
||||
<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">
|
||||
{flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin v2'}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{categories.slice(0, 4).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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import type { FlowType } from '../types'
|
||||
import { STUDIO_SCREENS, ADMIN_SCREENS } from '../data'
|
||||
|
||||
interface FlowTypeSelectorProps {
|
||||
flowType: FlowType
|
||||
onFlowTypeChange: (type: FlowType) => void
|
||||
}
|
||||
|
||||
export function FlowTypeSelector({ flowType, onFlowTypeChange }: FlowTypeSelectorProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => onFlowTypeChange('studio')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'studio'
|
||||
? 'border-green-500 bg-green-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'studio' ? 'bg-green-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
🎓
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Studio (Port 8000)</div>
|
||||
<div className="text-sm text-slate-500">Lehrer-Oberflaeche</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{STUDIO_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onFlowTypeChange('admin')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'admin'
|
||||
? 'border-primary-500 bg-primary-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'admin' ? 'bg-primary-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
⚙️
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Admin v2 (Port 3002)</div>
|
||||
<div className="text-sm text-slate-500">Admin Panel</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{ADMIN_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import type { ScreenDefinition } from '../types'
|
||||
|
||||
interface ScreenListProps {
|
||||
screens: ScreenDefinition[]
|
||||
colors: Record<string, { bg: string; border: string; text: string }>
|
||||
labels: Record<string, string>
|
||||
baseUrl: string
|
||||
selectedCategory: string | null
|
||||
onSelectScreen: (screen: ScreenDefinition) => void
|
||||
}
|
||||
|
||||
export function ScreenList({
|
||||
screens,
|
||||
colors,
|
||||
labels,
|
||||
baseUrl,
|
||||
selectedCategory,
|
||||
onSelectScreen,
|
||||
}: ScreenListProps) {
|
||||
const filtered = selectedCategory
|
||||
? screens.filter(s => s.category === selectedCategory)
|
||||
: screens
|
||||
|
||||
return (
|
||||
<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">
|
||||
{filtered.map((screen) => {
|
||||
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
|
||||
return (
|
||||
<button
|
||||
key={screen.id}
|
||||
onClick={() => onSelectScreen(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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import type { ScreenDefinition } from '../types'
|
||||
|
||||
interface StatsBarProps {
|
||||
totalScreens: number
|
||||
totalConnections: number
|
||||
connectedCount: number
|
||||
selectedNode: string | null
|
||||
previewScreen: ScreenDefinition | null
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
export function StatsBar({
|
||||
totalScreens,
|
||||
totalConnections,
|
||||
connectedCount,
|
||||
selectedNode,
|
||||
previewScreen,
|
||||
onReset,
|
||||
}: StatsBarProps) {
|
||||
return (
|
||||
<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">{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-primary-600">{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">
|
||||
{connectedCount} verbundene Screen{connectedCount !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onReset}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { FlowTypeSelector } from './FlowTypeSelector'
|
||||
export { StatsBar } from './StatsBar'
|
||||
export { CategoryFilter } from './CategoryFilter'
|
||||
export { ConnectedScreensList } from './ConnectedScreensList'
|
||||
export { FlowDiagram } from './FlowDiagram'
|
||||
export { ScreenList } from './ScreenList'
|
||||
264
admin-lehrer/app/(admin)/development/screen-flow/data.ts
Normal file
264
admin-lehrer/app/(admin)/development/screen-flow/data.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Screen Flow - Screen definitions, connections, colors, labels
|
||||
*/
|
||||
|
||||
import type { ScreenDefinition, ConnectionDef } from './types'
|
||||
|
||||
// ============================================
|
||||
// STUDIO SCREENS (Port 8000)
|
||||
// ============================================
|
||||
|
||||
export const STUDIO_SCREENS: ScreenDefinition[] = [
|
||||
{ id: 'lehrer-dashboard', name: 'Mein Dashboard', description: 'Hauptuebersicht mit Widgets', category: 'navigation', icon: '🏠', url: '/app#lehrer-dashboard' },
|
||||
{ id: 'lehrer-onboarding', name: 'Erste Schritte', description: 'Onboarding & Schnellstart', category: 'navigation', icon: '🚀', url: '/app#lehrer-onboarding' },
|
||||
{ id: 'hilfe', name: 'Dokumentation', description: 'Hilfe & Anleitungen', category: 'navigation', icon: '📚', url: '/app#hilfe' },
|
||||
{ id: 'worksheets', name: 'Arbeitsblaetter Studio', description: 'Lernmaterialien erstellen', category: 'content', icon: '📝', url: '/app#worksheets' },
|
||||
{ id: 'content-creator', name: 'Content Creator', description: 'Inhalte erstellen', category: 'content', icon: '✨', url: '/app#content-creator' },
|
||||
{ id: 'content-feed', name: 'Content Feed', description: 'Inhalte durchsuchen', category: 'content', icon: '📰', url: '/app#content-feed' },
|
||||
{ id: 'unit-creator', name: 'Unit Creator', description: 'Lerneinheiten erstellen', category: 'content', icon: '📦', url: '/app#unit-creator' },
|
||||
{ id: 'letters', name: 'Briefe & Vorlagen', description: 'Brief-Generator', category: 'content', icon: '✉️', url: '/app#letters' },
|
||||
{ id: 'correction', name: 'Korrektur', description: 'Arbeiten korrigieren', category: 'content', icon: '✏️', url: '/app#correction' },
|
||||
{ id: 'klausur-korrektur', name: 'Abiturklausuren', description: 'KI-gestuetzte Klausurkorrektur', category: 'content', icon: '📋', url: '/app#klausur-korrektur' },
|
||||
{ id: 'jitsi', name: 'Videokonferenz', description: 'Jitsi Meet Integration', category: 'communication', icon: '🎥', url: '/app#jitsi' },
|
||||
{ id: 'messenger', name: 'Messenger', description: 'Matrix E2EE Chat', category: 'communication', icon: '💬', url: '/app#messenger' },
|
||||
{ id: 'mail', name: 'Unified Inbox', description: 'E-Mail Verwaltung', category: 'communication', icon: '📧', url: '/app#mail' },
|
||||
{ id: 'school-classes', name: 'Klassen', description: 'Klassenverwaltung', category: 'school', icon: '👥', url: '/app#school-classes' },
|
||||
{ id: 'school-exams', name: 'Pruefungen', description: 'Pruefungsverwaltung', category: 'school', icon: '📝', url: '/app#school-exams' },
|
||||
{ id: 'school-grades', name: 'Noten', description: 'Notenverwaltung', category: 'school', icon: '📊', url: '/app#school-grades' },
|
||||
{ id: 'school-gradebook', name: 'Notenbuch', description: 'Digitales Notenbuch', category: 'school', icon: '📖', url: '/app#school-gradebook' },
|
||||
{ id: 'school-certificates', name: 'Zeugnisse', description: 'Zeugniserstellung', category: 'school', icon: '🎓', url: '/app#school-certificates' },
|
||||
{ id: 'companion', name: 'Begleiter & Stunde', description: 'KI-Unterrichtsassistent', category: 'ai', icon: '🤖', url: '/app#companion' },
|
||||
{ id: 'alerts', name: 'Alerts', description: 'News & Benachrichtigungen', category: 'ai', icon: '🔔', url: '/app#alerts' },
|
||||
{ id: 'admin', name: 'Einstellungen', description: 'Systemeinstellungen', category: 'admin', icon: '⚙️', url: '/app#admin' },
|
||||
{ id: 'rbac-admin', name: 'Rollen & Rechte', description: 'Berechtigungsverwaltung', category: 'admin', icon: '🔐', url: '/app#rbac-admin' },
|
||||
{ id: 'abitur-docs-admin', name: 'Abitur Dokumente', description: 'Erwartungshorizonte', category: 'admin', icon: '📄', url: '/app#abitur-docs-admin' },
|
||||
{ id: 'system-info', name: 'System Info', description: 'Systeminformationen', category: 'admin', icon: '💻', url: '/app#system-info' },
|
||||
{ id: 'workflow', name: 'Workflow', description: 'Automatisierungen', category: 'admin', icon: '⚡', url: '/app#workflow' },
|
||||
]
|
||||
|
||||
export const STUDIO_CONNECTIONS: ConnectionDef[] = [
|
||||
{ source: 'lehrer-onboarding', target: 'worksheets', label: 'Arbeitsblaetter' },
|
||||
{ source: 'lehrer-onboarding', target: 'klausur-korrektur', label: 'Abiturklausuren' },
|
||||
{ source: 'lehrer-onboarding', target: 'correction', label: 'Korrektur' },
|
||||
{ source: 'lehrer-onboarding', target: 'letters', label: 'Briefe' },
|
||||
{ source: 'lehrer-onboarding', target: 'school-classes', label: 'Klassen' },
|
||||
{ source: 'lehrer-onboarding', target: 'jitsi', label: 'Meet' },
|
||||
{ source: 'lehrer-onboarding', target: 'hilfe', label: 'Doku' },
|
||||
{ source: 'lehrer-onboarding', target: 'admin', label: 'Settings' },
|
||||
{ source: 'lehrer-dashboard', target: 'worksheets' },
|
||||
{ source: 'lehrer-dashboard', target: 'correction' },
|
||||
{ source: 'lehrer-dashboard', target: 'jitsi' },
|
||||
{ source: 'lehrer-dashboard', target: 'letters' },
|
||||
{ source: 'lehrer-dashboard', target: 'messenger' },
|
||||
{ source: 'lehrer-dashboard', target: 'klausur-korrektur' },
|
||||
{ source: 'lehrer-dashboard', target: 'companion' },
|
||||
{ source: 'lehrer-dashboard', target: 'alerts' },
|
||||
{ source: 'lehrer-dashboard', target: 'mail' },
|
||||
{ source: 'lehrer-dashboard', target: 'school-classes' },
|
||||
{ source: 'lehrer-dashboard', target: 'lehrer-onboarding', label: 'Sidebar' },
|
||||
{ source: 'school-classes', target: 'school-exams' },
|
||||
{ source: 'school-classes', target: 'school-grades' },
|
||||
{ source: 'school-grades', target: 'school-gradebook' },
|
||||
{ source: 'school-gradebook', target: 'school-certificates' },
|
||||
{ source: 'worksheets', target: 'content-creator' },
|
||||
{ source: 'worksheets', target: 'unit-creator' },
|
||||
{ source: 'content-creator', target: 'content-feed' },
|
||||
{ source: 'klausur-korrektur', target: 'abitur-docs-admin' },
|
||||
{ source: 'admin', target: 'rbac-admin' },
|
||||
{ source: 'admin', target: 'system-info' },
|
||||
{ source: 'admin', target: 'workflow' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// ADMIN v2 SCREENS (Port 3002)
|
||||
// Based on navigation.ts - Last updated: 2026-02-03
|
||||
// ============================================
|
||||
|
||||
export const ADMIN_SCREENS: ScreenDefinition[] = [
|
||||
// === META / OVERVIEW ===
|
||||
{ id: 'admin-dashboard', name: 'Dashboard', description: 'Uebersicht & Statistiken', category: 'overview', icon: '🏠', url: '/dashboard' },
|
||||
{ id: 'admin-onboarding', name: 'Onboarding', description: 'Lern-Wizards fuer alle Module', category: 'overview', icon: '📖', url: '/onboarding' },
|
||||
{ id: 'admin-architecture', name: 'Architektur', description: 'Backend-Module & Datenfluss', category: 'overview', icon: '🏗️', url: '/architecture' },
|
||||
{ id: 'admin-backlog', name: 'Production Backlog', description: 'Go-Live Checkliste', category: 'overview', icon: '📝', url: '/backlog' },
|
||||
{ id: 'admin-rbac', name: 'RBAC', description: 'Rollen & Berechtigungen', category: 'overview', icon: '👥', url: '/rbac' },
|
||||
|
||||
// === COMPLIANCE SDK (Violet #8b5cf6) ===
|
||||
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'sdk', icon: '📄', url: '/sdk/consent-management' },
|
||||
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'sdk', icon: '🔒', url: '/sdk/dsr' },
|
||||
{ id: 'admin-einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'sdk', icon: '✅', url: '/sdk/einwilligungen' },
|
||||
{ id: 'admin-vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'sdk', icon: '📋', url: '/sdk/vvt' },
|
||||
{ id: 'admin-dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'sdk', icon: '⚖️', url: '/sdk/dsfa' },
|
||||
{ id: 'admin-tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'sdk', icon: '🛡️', url: '/sdk/tom' },
|
||||
{ id: 'admin-loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'sdk', icon: '🗑️', url: '/sdk/loeschfristen' },
|
||||
{ id: 'admin-advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'sdk', icon: '🧑⚖️', url: '/sdk/advisory-board' },
|
||||
{ id: 'admin-escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'sdk', icon: '🚨', url: '/sdk/escalations' },
|
||||
{ id: 'admin-compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'sdk', icon: '✅', url: '/sdk/compliance-hub' },
|
||||
{ id: 'admin-audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'sdk', icon: '📋', url: '/sdk/audit-checklist' },
|
||||
{ id: 'admin-requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'sdk', icon: '📜', url: '/sdk/requirements' },
|
||||
{ id: 'admin-controls', name: 'Controls', description: '474 Control-Mappings', category: 'sdk', icon: '🎛️', url: '/sdk/controls' },
|
||||
{ id: 'admin-evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'sdk', icon: '📎', url: '/sdk/evidence' },
|
||||
{ id: 'admin-risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'sdk', icon: '⚠️', url: '/sdk/risks' },
|
||||
{ id: 'admin-audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'sdk', icon: '📊', url: '/sdk/audit-report' },
|
||||
{ id: 'admin-modules', name: 'Service Registry', description: '30+ Service-Module', category: 'sdk', icon: '🔧', url: '/sdk/modules' },
|
||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'sdk', icon: '🏛️', url: '/sdk/dsms' },
|
||||
{ id: 'admin-compliance-workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'sdk', icon: '🔄', url: '/sdk/workflow' },
|
||||
{ id: 'admin-source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'sdk', icon: '📚', url: '/sdk/source-policy' },
|
||||
{ id: 'admin-ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'sdk', icon: '🤖', url: '/sdk/ai-act' },
|
||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
||||
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/ai/rag' },
|
||||
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '✍️', url: '/ai/ocr-labeling' },
|
||||
{ id: 'admin-magic-help', name: 'Magic Help', description: 'TrOCR Handschrift-OCR', category: 'ai', icon: '🪄', url: '/ai/magic-help' },
|
||||
{ id: 'admin-klausur-korrektur', name: 'Klausur-Korrektur', description: 'Abitur-Korrektur mit KI', category: 'ai', icon: '📝', url: '/ai/klausur-korrektur' },
|
||||
{ id: 'admin-quality', name: 'Qualitaet & Audit', description: 'Compliance-Audit & Traceability', category: 'ai', icon: '✨', url: '/ai/quality' },
|
||||
{ id: 'admin-test-quality', name: 'Test Quality (BQAS)', description: 'Golden Suite & Synthetic Tests', category: 'ai', icon: '🧪', url: '/ai/test-quality' },
|
||||
{ id: 'admin-agents', name: 'Agent Management', description: 'Multi-Agent & SOUL-Editor', category: 'ai', icon: '🧠', url: '/ai/agents' },
|
||||
|
||||
// === INFRASTRUKTUR (Orange #f97316) ===
|
||||
{ id: 'admin-gpu', name: 'GPU Infrastruktur', description: 'vast.ai GPU Management', category: 'infrastructure', icon: '🖥️', url: '/infrastructure/gpu' },
|
||||
{ id: 'admin-middleware', name: 'Middleware', description: 'Stack & API Gateway', category: 'infrastructure', icon: '🔧', url: '/infrastructure/middleware' },
|
||||
{ id: 'admin-security', name: 'Security', description: 'DevSecOps & Scans', category: 'infrastructure', icon: '🔐', url: '/infrastructure/security' },
|
||||
{ id: 'admin-sbom', name: 'SBOM', description: 'Software Bill of Materials', category: 'infrastructure', icon: '📦', url: '/infrastructure/sbom' },
|
||||
{ id: 'admin-cicd', name: 'CI/CD', description: 'Pipelines & Deployments', category: 'infrastructure', icon: '🔄', url: '/infrastructure/ci-cd' },
|
||||
{ id: 'admin-tests', name: 'Test Dashboard', description: '195+ Tests & Coverage', category: 'infrastructure', icon: '🧪', url: '/infrastructure/tests' },
|
||||
|
||||
// === BILDUNG (Blue #3b82f6) ===
|
||||
{ id: 'admin-edu-search', name: 'Education Search', description: 'Bildungsquellen & Crawler', category: 'education', icon: '🔍', url: '/education/edu-search' },
|
||||
{ id: 'admin-zeugnisse', name: 'Zeugnisse-Crawler', description: 'Zeugnis-Daten', category: 'education', icon: '📜', url: '/education/zeugnisse-crawler' },
|
||||
{ id: 'admin-rag-pipeline', name: 'RAG Pipeline', description: 'Bildungsdokumente indexieren', category: 'ai', icon: '🔗', url: '/ai/rag-pipeline' },
|
||||
{ id: 'admin-foerderantrag', name: 'Foerderantrag-Wizard', description: 'DigitalPakt & Landesfoerderung', category: 'education', icon: '💰', url: '/education/foerderantrag' },
|
||||
|
||||
// === KOMMUNIKATION (Green #22c55e) ===
|
||||
{ id: 'admin-video', name: 'Video & Chat', description: 'Matrix & Jitsi Monitoring', category: 'communication', icon: '🎥', url: '/communication/video-chat' },
|
||||
{ id: 'admin-matrix', name: 'Voice Service', description: 'Voice-First Interface', category: 'communication', icon: '🎙️', url: '/communication/matrix' },
|
||||
{ id: 'admin-mail', name: 'Unified Inbox', description: 'E-Mail & KI-Analyse', category: 'communication', icon: '📧', url: '/communication/mail' },
|
||||
{ id: 'admin-alerts', name: 'Alerts Monitoring', description: 'Google Alerts & Feeds', category: 'communication', icon: '🔔', url: '/communication/alerts' },
|
||||
|
||||
// === ENTWICKLUNG (Slate #64748b) ===
|
||||
{ id: 'admin-workflow', name: 'Dev Workflow', description: 'Git, CI/CD & Team-Regeln', category: 'development', icon: '⚡', url: '/development/workflow' },
|
||||
{ id: 'admin-game', name: 'Breakpilot Drive', description: 'Lernspiel Management', category: 'development', icon: '🎮', url: '/development/game' },
|
||||
{ id: 'admin-unity', name: 'Unity Bridge', description: 'Unity Editor Steuerung', category: 'development', icon: '🎯', url: '/development/unity-bridge' },
|
||||
{ id: 'admin-companion', name: 'Companion Dev', description: 'Lesson-Modus Entwicklung', category: 'development', icon: '📚', url: '/development/companion' },
|
||||
{ id: 'admin-docs', name: 'Developer Docs', description: 'API & Architektur', category: 'development', icon: '📖', url: '/development/docs' },
|
||||
{ id: 'admin-brandbook', name: 'Brandbook', description: 'Corporate Design', category: 'development', icon: '🎨', url: '/development/brandbook' },
|
||||
{ id: 'admin-screen-flow', name: 'Screen Flow', description: 'UI Screen-Verbindungen', category: 'development', icon: '🔀', url: '/development/screen-flow' },
|
||||
{ id: 'admin-content', name: 'Uebersetzungen', description: 'Website Content & Sprachen', category: 'development', icon: '🌐', url: '/development/content' },
|
||||
]
|
||||
|
||||
export const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
||||
// === OVERVIEW/META FLOWS ===
|
||||
{ source: 'admin-dashboard', target: 'admin-onboarding', label: 'Erste Schritte' },
|
||||
{ source: 'admin-dashboard', target: 'admin-architecture', label: 'System' },
|
||||
{ source: 'admin-dashboard', target: 'admin-backlog', label: 'Go-Live' },
|
||||
{ source: 'admin-dashboard', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
{ source: 'admin-onboarding', target: 'admin-consent' },
|
||||
{ source: 'admin-rbac', target: 'admin-consent' },
|
||||
|
||||
// === DSGVO FLOW ===
|
||||
{ source: 'admin-consent', target: 'admin-einwilligungen', label: 'Nutzer' },
|
||||
{ source: 'admin-consent', target: 'admin-dsr' },
|
||||
{ source: 'admin-dsr', target: 'admin-loeschfristen' },
|
||||
{ source: 'admin-vvt', target: 'admin-tom' },
|
||||
{ source: 'admin-vvt', target: 'admin-dsfa' },
|
||||
{ source: 'admin-dsfa', target: 'admin-tom' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-escalations', label: 'Eskalation' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-dsfa', label: 'Risiko' },
|
||||
|
||||
// === COMPLIANCE FLOW ===
|
||||
{ source: 'admin-compliance-hub', target: 'admin-audit-checklist', label: 'Audit' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-requirements', label: 'Anforderungen' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-risks', label: 'Risiken' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-ai-act', label: 'AI Act' },
|
||||
{ source: 'admin-requirements', target: 'admin-controls' },
|
||||
{ source: 'admin-controls', target: 'admin-evidence' },
|
||||
{ source: 'admin-audit-checklist', target: 'admin-audit-report', label: 'Report' },
|
||||
{ source: 'admin-risks', target: 'admin-controls' },
|
||||
{ source: 'admin-modules', target: 'admin-controls' },
|
||||
{ source: 'admin-source-policy', target: 'admin-rag' },
|
||||
{ source: 'admin-obligations', target: 'admin-requirements' },
|
||||
{ source: 'admin-dsms', target: 'admin-compliance-workflow' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG FLOW ===
|
||||
{ source: 'admin-rag', target: 'admin-quality' },
|
||||
{ source: 'admin-rag', target: 'admin-agents' },
|
||||
{ source: 'admin-ocr-labeling', target: 'admin-magic-help', label: 'Training' },
|
||||
{ source: 'admin-magic-help', target: 'admin-klausur-korrektur', label: 'Korrektur' },
|
||||
{ source: 'admin-quality', target: 'admin-test-quality' },
|
||||
{ source: 'admin-agents', target: 'admin-test-quality', label: 'BQAS' },
|
||||
{ source: 'admin-klausur-korrektur', target: 'admin-quality', label: 'Audit' },
|
||||
|
||||
// === INFRASTRUKTUR FLOW ===
|
||||
{ source: 'admin-security', target: 'admin-sbom', label: 'Dependencies' },
|
||||
{ source: 'admin-sbom', target: 'admin-tests' },
|
||||
{ source: 'admin-tests', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-cicd', target: 'admin-middleware' },
|
||||
{ source: 'admin-middleware', target: 'admin-gpu', label: 'GPU' },
|
||||
{ source: 'admin-security', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
|
||||
// === BILDUNG FLOW ===
|
||||
{ source: 'admin-edu-search', target: 'admin-rag', label: 'Quellen' },
|
||||
{ source: 'admin-edu-search', target: 'admin-zeugnisse' },
|
||||
{ source: 'admin-training', target: 'admin-onboarding' },
|
||||
{ source: 'admin-foerderantrag', target: 'admin-docs', label: 'Docs' },
|
||||
|
||||
// === KOMMUNIKATION FLOW ===
|
||||
{ source: 'admin-video', target: 'admin-matrix', label: 'Voice' },
|
||||
{ source: 'admin-mail', target: 'admin-alerts' },
|
||||
{ source: 'admin-alerts', target: 'admin-mail', label: 'Inbox' },
|
||||
|
||||
// === ENTWICKLUNG FLOW ===
|
||||
{ source: 'admin-workflow', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-workflow', target: 'admin-docs' },
|
||||
{ source: 'admin-game', target: 'admin-unity', label: 'Editor' },
|
||||
{ source: 'admin-companion', target: 'admin-agents', label: 'Agents' },
|
||||
{ source: 'admin-brandbook', target: 'admin-screen-flow' },
|
||||
{ source: 'admin-docs', target: 'admin-architecture' },
|
||||
{ source: 'admin-content', target: 'admin-brandbook' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// CATEGORY COLORS
|
||||
// ============================================
|
||||
|
||||
export const STUDIO_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
navigation: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
|
||||
content: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
|
||||
communication: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
|
||||
school: { bg: '#fce7f3', border: '#ec4899', text: '#9d174d' },
|
||||
admin: { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
|
||||
ai: { bg: '#cffafe', border: '#06b6d4', text: '#0e7490' },
|
||||
}
|
||||
|
||||
// Colors from navigation.ts
|
||||
export const ADMIN_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
overview: { bg: '#e0f2fe', border: '#0ea5e9', text: '#0369a1' },
|
||||
dsgvo: { bg: '#ede9fe', border: '#7c3aed', text: '#5b21b6' },
|
||||
compliance: { bg: '#f3e8ff', border: '#9333ea', text: '#6b21a8' },
|
||||
ai: { bg: '#ccfbf1', border: '#14b8a6', text: '#0f766e' },
|
||||
infrastructure: { bg: '#ffedd5', border: '#f97316', text: '#c2410c' },
|
||||
education: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
|
||||
communication: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
|
||||
development: { bg: '#f1f5f9', border: '#64748b', text: '#334155' },
|
||||
}
|
||||
|
||||
export const STUDIO_LABELS: Record<string, string> = {
|
||||
navigation: 'Navigation',
|
||||
content: 'Content & Tools',
|
||||
communication: 'Kommunikation',
|
||||
school: 'Schulverwaltung',
|
||||
admin: 'Administration',
|
||||
ai: 'KI & Assistent',
|
||||
}
|
||||
|
||||
// Labels from navigation.ts
|
||||
export const ADMIN_LABELS: Record<string, string> = {
|
||||
overview: 'Uebersicht & Meta',
|
||||
dsgvo: 'DSGVO',
|
||||
compliance: 'Compliance & GRC',
|
||||
ai: 'KI & Automatisierung',
|
||||
infrastructure: 'Infrastruktur & DevOps',
|
||||
education: 'Bildung & Schule',
|
||||
communication: 'Kommunikation & Alerts',
|
||||
development: 'Entwicklung & Produkte',
|
||||
}
|
||||
83
admin-lehrer/app/(admin)/development/screen-flow/helpers.ts
Normal file
83
admin-lehrer/app/(admin)/development/screen-flow/helpers.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Screen Flow - Layout and graph traversal helpers
|
||||
*/
|
||||
|
||||
import type { ScreenDefinition, ConnectionDef, FlowType } from './types'
|
||||
|
||||
/**
|
||||
* BFS traversal to find all connected nodes from a start node.
|
||||
*/
|
||||
export 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
|
||||
}
|
||||
|
||||
const STUDIO_POSITIONS: Record<string, { x: number; y: number }> = {
|
||||
navigation: { x: 400, y: 50 },
|
||||
content: { x: 50, y: 250 },
|
||||
communication: { x: 750, y: 250 },
|
||||
school: { x: 50, y: 500 },
|
||||
admin: { x: 750, y: 500 },
|
||||
ai: { x: 400, y: 380 },
|
||||
}
|
||||
|
||||
const ADMIN_POSITIONS: Record<string, { x: number; y: number }> = {
|
||||
overview: { x: 400, y: 30 },
|
||||
dsgvo: { x: 50, y: 150 },
|
||||
compliance: { x: 700, y: 150 },
|
||||
ai: { x: 50, y: 350 },
|
||||
communication: { x: 400, y: 350 },
|
||||
infrastructure: { x: 700, y: 350 },
|
||||
education: { x: 50, y: 550 },
|
||||
development: { x: 400, y: 550 },
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the x/y position for a node based on its category and index.
|
||||
*/
|
||||
export function getNodePosition(
|
||||
id: string,
|
||||
category: string,
|
||||
screens: ScreenDefinition[],
|
||||
flowType: FlowType
|
||||
) {
|
||||
const positions = flowType === 'studio' ? STUDIO_POSITIONS : ADMIN_POSITIONS
|
||||
const base = 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,
|
||||
}
|
||||
}
|
||||
@@ -8,787 +8,122 @@
|
||||
* - Admin v2 (Port 3002): Admin Panel
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type FlowType = 'studio' | 'admin'
|
||||
|
||||
// ============================================
|
||||
// STUDIO SCREENS (Port 8000)
|
||||
// ============================================
|
||||
|
||||
const STUDIO_SCREENS: ScreenDefinition[] = [
|
||||
{ id: 'lehrer-dashboard', name: 'Mein Dashboard', description: 'Hauptuebersicht mit Widgets', category: 'navigation', icon: '🏠', url: '/app#lehrer-dashboard' },
|
||||
{ id: 'lehrer-onboarding', name: 'Erste Schritte', description: 'Onboarding & Schnellstart', category: 'navigation', icon: '🚀', url: '/app#lehrer-onboarding' },
|
||||
{ id: 'hilfe', name: 'Dokumentation', description: 'Hilfe & Anleitungen', category: 'navigation', icon: '📚', url: '/app#hilfe' },
|
||||
{ id: 'worksheets', name: 'Arbeitsblaetter Studio', description: 'Lernmaterialien erstellen', category: 'content', icon: '📝', url: '/app#worksheets' },
|
||||
{ id: 'content-creator', name: 'Content Creator', description: 'Inhalte erstellen', category: 'content', icon: '✨', url: '/app#content-creator' },
|
||||
{ id: 'content-feed', name: 'Content Feed', description: 'Inhalte durchsuchen', category: 'content', icon: '📰', url: '/app#content-feed' },
|
||||
{ id: 'unit-creator', name: 'Unit Creator', description: 'Lerneinheiten erstellen', category: 'content', icon: '📦', url: '/app#unit-creator' },
|
||||
{ id: 'letters', name: 'Briefe & Vorlagen', description: 'Brief-Generator', category: 'content', icon: '✉️', url: '/app#letters' },
|
||||
{ id: 'correction', name: 'Korrektur', description: 'Arbeiten korrigieren', category: 'content', icon: '✏️', url: '/app#correction' },
|
||||
{ id: 'klausur-korrektur', name: 'Abiturklausuren', description: 'KI-gestuetzte Klausurkorrektur', category: 'content', icon: '📋', url: '/app#klausur-korrektur' },
|
||||
{ id: 'jitsi', name: 'Videokonferenz', description: 'Jitsi Meet Integration', category: 'communication', icon: '🎥', url: '/app#jitsi' },
|
||||
{ id: 'messenger', name: 'Messenger', description: 'Matrix E2EE Chat', category: 'communication', icon: '💬', url: '/app#messenger' },
|
||||
{ id: 'mail', name: 'Unified Inbox', description: 'E-Mail Verwaltung', category: 'communication', icon: '📧', url: '/app#mail' },
|
||||
{ id: 'school-classes', name: 'Klassen', description: 'Klassenverwaltung', category: 'school', icon: '👥', url: '/app#school-classes' },
|
||||
{ id: 'school-exams', name: 'Pruefungen', description: 'Pruefungsverwaltung', category: 'school', icon: '📝', url: '/app#school-exams' },
|
||||
{ id: 'school-grades', name: 'Noten', description: 'Notenverwaltung', category: 'school', icon: '📊', url: '/app#school-grades' },
|
||||
{ id: 'school-gradebook', name: 'Notenbuch', description: 'Digitales Notenbuch', category: 'school', icon: '📖', url: '/app#school-gradebook' },
|
||||
{ id: 'school-certificates', name: 'Zeugnisse', description: 'Zeugniserstellung', category: 'school', icon: '🎓', url: '/app#school-certificates' },
|
||||
{ id: 'companion', name: 'Begleiter & Stunde', description: 'KI-Unterrichtsassistent', category: 'ai', icon: '🤖', url: '/app#companion' },
|
||||
{ id: 'alerts', name: 'Alerts', description: 'News & Benachrichtigungen', category: 'ai', icon: '🔔', url: '/app#alerts' },
|
||||
{ id: 'admin', name: 'Einstellungen', description: 'Systemeinstellungen', category: 'admin', icon: '⚙️', url: '/app#admin' },
|
||||
{ id: 'rbac-admin', name: 'Rollen & Rechte', description: 'Berechtigungsverwaltung', category: 'admin', icon: '🔐', url: '/app#rbac-admin' },
|
||||
{ id: 'abitur-docs-admin', name: 'Abitur Dokumente', description: 'Erwartungshorizonte', category: 'admin', icon: '📄', url: '/app#abitur-docs-admin' },
|
||||
{ id: 'system-info', name: 'System Info', description: 'Systeminformationen', category: 'admin', icon: '💻', url: '/app#system-info' },
|
||||
{ id: 'workflow', name: 'Workflow', description: 'Automatisierungen', category: 'admin', icon: '⚡', url: '/app#workflow' },
|
||||
]
|
||||
|
||||
const STUDIO_CONNECTIONS: ConnectionDef[] = [
|
||||
{ source: 'lehrer-onboarding', target: 'worksheets', label: 'Arbeitsblaetter' },
|
||||
{ source: 'lehrer-onboarding', target: 'klausur-korrektur', label: 'Abiturklausuren' },
|
||||
{ source: 'lehrer-onboarding', target: 'correction', label: 'Korrektur' },
|
||||
{ source: 'lehrer-onboarding', target: 'letters', label: 'Briefe' },
|
||||
{ source: 'lehrer-onboarding', target: 'school-classes', label: 'Klassen' },
|
||||
{ source: 'lehrer-onboarding', target: 'jitsi', label: 'Meet' },
|
||||
{ source: 'lehrer-onboarding', target: 'hilfe', label: 'Doku' },
|
||||
{ source: 'lehrer-onboarding', target: 'admin', label: 'Settings' },
|
||||
{ source: 'lehrer-dashboard', target: 'worksheets' },
|
||||
{ source: 'lehrer-dashboard', target: 'correction' },
|
||||
{ source: 'lehrer-dashboard', target: 'jitsi' },
|
||||
{ source: 'lehrer-dashboard', target: 'letters' },
|
||||
{ source: 'lehrer-dashboard', target: 'messenger' },
|
||||
{ source: 'lehrer-dashboard', target: 'klausur-korrektur' },
|
||||
{ source: 'lehrer-dashboard', target: 'companion' },
|
||||
{ source: 'lehrer-dashboard', target: 'alerts' },
|
||||
{ source: 'lehrer-dashboard', target: 'mail' },
|
||||
{ source: 'lehrer-dashboard', target: 'school-classes' },
|
||||
{ source: 'lehrer-dashboard', target: 'lehrer-onboarding', label: 'Sidebar' },
|
||||
{ source: 'school-classes', target: 'school-exams' },
|
||||
{ source: 'school-classes', target: 'school-grades' },
|
||||
{ source: 'school-grades', target: 'school-gradebook' },
|
||||
{ source: 'school-gradebook', target: 'school-certificates' },
|
||||
{ source: 'worksheets', target: 'content-creator' },
|
||||
{ source: 'worksheets', target: 'unit-creator' },
|
||||
{ source: 'content-creator', target: 'content-feed' },
|
||||
{ source: 'klausur-korrektur', target: 'abitur-docs-admin' },
|
||||
{ source: 'admin', target: 'rbac-admin' },
|
||||
{ source: 'admin', target: 'system-info' },
|
||||
{ source: 'admin', target: 'workflow' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// ADMIN v2 SCREENS (Port 3002)
|
||||
// Based on navigation.ts - Last updated: 2026-02-03
|
||||
// ============================================
|
||||
|
||||
const ADMIN_SCREENS: ScreenDefinition[] = [
|
||||
// === META / OVERVIEW ===
|
||||
{ id: 'admin-dashboard', name: 'Dashboard', description: 'Uebersicht & Statistiken', category: 'overview', icon: '🏠', url: '/dashboard' },
|
||||
{ id: 'admin-onboarding', name: 'Onboarding', description: 'Lern-Wizards fuer alle Module', category: 'overview', icon: '📖', url: '/onboarding' },
|
||||
{ id: 'admin-architecture', name: 'Architektur', description: 'Backend-Module & Datenfluss', category: 'overview', icon: '🏗️', url: '/architecture' },
|
||||
{ id: 'admin-backlog', name: 'Production Backlog', description: 'Go-Live Checkliste', category: 'overview', icon: '📝', url: '/backlog' },
|
||||
{ id: 'admin-rbac', name: 'RBAC', description: 'Rollen & Berechtigungen', category: 'overview', icon: '👥', url: '/rbac' },
|
||||
|
||||
// === COMPLIANCE SDK (Violet #8b5cf6) ===
|
||||
// DSGVO - Datenschutz & Betroffenenrechte
|
||||
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'sdk', icon: '📄', url: '/sdk/consent-management' },
|
||||
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'sdk', icon: '🔒', url: '/sdk/dsr' },
|
||||
{ id: 'admin-einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'sdk', icon: '✅', url: '/sdk/einwilligungen' },
|
||||
{ id: 'admin-vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'sdk', icon: '📋', url: '/sdk/vvt' },
|
||||
{ id: 'admin-dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'sdk', icon: '⚖️', url: '/sdk/dsfa' },
|
||||
{ id: 'admin-tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'sdk', icon: '🛡️', url: '/sdk/tom' },
|
||||
{ id: 'admin-loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'sdk', icon: '🗑️', url: '/sdk/loeschfristen' },
|
||||
{ id: 'admin-advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'sdk', icon: '🧑⚖️', url: '/sdk/advisory-board' },
|
||||
{ id: 'admin-escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'sdk', icon: '🚨', url: '/sdk/escalations' },
|
||||
// Compliance - Audit, GRC & Regulatorik
|
||||
{ id: 'admin-compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'sdk', icon: '✅', url: '/sdk/compliance-hub' },
|
||||
{ id: 'admin-audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'sdk', icon: '📋', url: '/sdk/audit-checklist' },
|
||||
{ id: 'admin-requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'sdk', icon: '📜', url: '/sdk/requirements' },
|
||||
{ id: 'admin-controls', name: 'Controls', description: '474 Control-Mappings', category: 'sdk', icon: '🎛️', url: '/sdk/controls' },
|
||||
{ id: 'admin-evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'sdk', icon: '📎', url: '/sdk/evidence' },
|
||||
{ id: 'admin-risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'sdk', icon: '⚠️', url: '/sdk/risks' },
|
||||
{ id: 'admin-audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'sdk', icon: '📊', url: '/sdk/audit-report' },
|
||||
{ id: 'admin-modules', name: 'Service Registry', description: '30+ Service-Module', category: 'sdk', icon: '🔧', url: '/sdk/modules' },
|
||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'sdk', icon: '🏛️', url: '/sdk/dsms' },
|
||||
{ id: 'admin-compliance-workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'sdk', icon: '🔄', url: '/sdk/workflow' },
|
||||
{ id: 'admin-source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'sdk', icon: '📚', url: '/sdk/source-policy' },
|
||||
{ id: 'admin-ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'sdk', icon: '🤖', url: '/sdk/ai-act' },
|
||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
||||
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/ai/rag' },
|
||||
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '✍️', url: '/ai/ocr-labeling' },
|
||||
{ id: 'admin-magic-help', name: 'Magic Help', description: 'TrOCR Handschrift-OCR', category: 'ai', icon: '🪄', url: '/ai/magic-help' },
|
||||
{ id: 'admin-klausur-korrektur', name: 'Klausur-Korrektur', description: 'Abitur-Korrektur mit KI', category: 'ai', icon: '📝', url: '/ai/klausur-korrektur' },
|
||||
{ id: 'admin-quality', name: 'Qualitaet & Audit', description: 'Compliance-Audit & Traceability', category: 'ai', icon: '✨', url: '/ai/quality' },
|
||||
{ id: 'admin-test-quality', name: 'Test Quality (BQAS)', description: 'Golden Suite & Synthetic Tests', category: 'ai', icon: '🧪', url: '/ai/test-quality' },
|
||||
{ id: 'admin-agents', name: 'Agent Management', description: 'Multi-Agent & SOUL-Editor', category: 'ai', icon: '🧠', url: '/ai/agents' },
|
||||
|
||||
// === INFRASTRUKTUR (Orange #f97316) ===
|
||||
{ id: 'admin-gpu', name: 'GPU Infrastruktur', description: 'vast.ai GPU Management', category: 'infrastructure', icon: '🖥️', url: '/infrastructure/gpu' },
|
||||
{ id: 'admin-middleware', name: 'Middleware', description: 'Stack & API Gateway', category: 'infrastructure', icon: '🔧', url: '/infrastructure/middleware' },
|
||||
{ id: 'admin-security', name: 'Security', description: 'DevSecOps & Scans', category: 'infrastructure', icon: '🔐', url: '/infrastructure/security' },
|
||||
{ id: 'admin-sbom', name: 'SBOM', description: 'Software Bill of Materials', category: 'infrastructure', icon: '📦', url: '/infrastructure/sbom' },
|
||||
{ id: 'admin-cicd', name: 'CI/CD', description: 'Pipelines & Deployments', category: 'infrastructure', icon: '🔄', url: '/infrastructure/ci-cd' },
|
||||
{ id: 'admin-tests', name: 'Test Dashboard', description: '195+ Tests & Coverage', category: 'infrastructure', icon: '🧪', url: '/infrastructure/tests' },
|
||||
|
||||
// === BILDUNG (Blue #3b82f6) ===
|
||||
{ id: 'admin-edu-search', name: 'Education Search', description: 'Bildungsquellen & Crawler', category: 'education', icon: '🔍', url: '/education/edu-search' },
|
||||
{ id: 'admin-zeugnisse', name: 'Zeugnisse-Crawler', description: 'Zeugnis-Daten', category: 'education', icon: '📜', url: '/education/zeugnisse-crawler' },
|
||||
{ id: 'admin-rag-pipeline', name: 'RAG Pipeline', description: 'Bildungsdokumente indexieren', category: 'ai', icon: '🔗', url: '/ai/rag-pipeline' },
|
||||
{ id: 'admin-foerderantrag', name: 'Foerderantrag-Wizard', description: 'DigitalPakt & Landesfoerderung', category: 'education', icon: '💰', url: '/education/foerderantrag' },
|
||||
|
||||
// === KOMMUNIKATION (Green #22c55e) ===
|
||||
{ id: 'admin-video', name: 'Video & Chat', description: 'Matrix & Jitsi Monitoring', category: 'communication', icon: '🎥', url: '/communication/video-chat' },
|
||||
{ id: 'admin-matrix', name: 'Voice Service', description: 'Voice-First Interface', category: 'communication', icon: '🎙️', url: '/communication/matrix' },
|
||||
{ id: 'admin-mail', name: 'Unified Inbox', description: 'E-Mail & KI-Analyse', category: 'communication', icon: '📧', url: '/communication/mail' },
|
||||
{ id: 'admin-alerts', name: 'Alerts Monitoring', description: 'Google Alerts & Feeds', category: 'communication', icon: '🔔', url: '/communication/alerts' },
|
||||
|
||||
// === ENTWICKLUNG (Slate #64748b) ===
|
||||
{ id: 'admin-workflow', name: 'Dev Workflow', description: 'Git, CI/CD & Team-Regeln', category: 'development', icon: '⚡', url: '/development/workflow' },
|
||||
{ id: 'admin-game', name: 'Breakpilot Drive', description: 'Lernspiel Management', category: 'development', icon: '🎮', url: '/development/game' },
|
||||
{ id: 'admin-unity', name: 'Unity Bridge', description: 'Unity Editor Steuerung', category: 'development', icon: '🎯', url: '/development/unity-bridge' },
|
||||
{ id: 'admin-companion', name: 'Companion Dev', description: 'Lesson-Modus Entwicklung', category: 'development', icon: '📚', url: '/development/companion' },
|
||||
{ id: 'admin-docs', name: 'Developer Docs', description: 'API & Architektur', category: 'development', icon: '📖', url: '/development/docs' },
|
||||
{ id: 'admin-brandbook', name: 'Brandbook', description: 'Corporate Design', category: 'development', icon: '🎨', url: '/development/brandbook' },
|
||||
{ id: 'admin-screen-flow', name: 'Screen Flow', description: 'UI Screen-Verbindungen', category: 'development', icon: '🔀', url: '/development/screen-flow' },
|
||||
{ id: 'admin-content', name: 'Uebersetzungen', description: 'Website Content & Sprachen', category: 'development', icon: '🌐', url: '/development/content' },
|
||||
]
|
||||
|
||||
const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
||||
// === OVERVIEW/META FLOWS ===
|
||||
{ source: 'admin-dashboard', target: 'admin-onboarding', label: 'Erste Schritte' },
|
||||
{ source: 'admin-dashboard', target: 'admin-architecture', label: 'System' },
|
||||
{ source: 'admin-dashboard', target: 'admin-backlog', label: 'Go-Live' },
|
||||
{ source: 'admin-dashboard', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
{ source: 'admin-onboarding', target: 'admin-consent' },
|
||||
{ source: 'admin-rbac', target: 'admin-consent' },
|
||||
|
||||
// === DSGVO FLOW ===
|
||||
{ source: 'admin-consent', target: 'admin-einwilligungen', label: 'Nutzer' },
|
||||
{ source: 'admin-consent', target: 'admin-dsr' },
|
||||
{ source: 'admin-dsr', target: 'admin-loeschfristen' },
|
||||
{ source: 'admin-vvt', target: 'admin-tom' },
|
||||
{ source: 'admin-vvt', target: 'admin-dsfa' },
|
||||
{ source: 'admin-dsfa', target: 'admin-tom' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-escalations', label: 'Eskalation' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-dsfa', label: 'Risiko' },
|
||||
|
||||
// === COMPLIANCE FLOW ===
|
||||
{ source: 'admin-compliance-hub', target: 'admin-audit-checklist', label: 'Audit' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-requirements', label: 'Anforderungen' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-risks', label: 'Risiken' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-ai-act', label: 'AI Act' },
|
||||
{ source: 'admin-requirements', target: 'admin-controls' },
|
||||
{ source: 'admin-controls', target: 'admin-evidence' },
|
||||
{ source: 'admin-audit-checklist', target: 'admin-audit-report', label: 'Report' },
|
||||
{ source: 'admin-risks', target: 'admin-controls' },
|
||||
{ source: 'admin-modules', target: 'admin-controls' },
|
||||
{ source: 'admin-source-policy', target: 'admin-rag' },
|
||||
{ source: 'admin-obligations', target: 'admin-requirements' },
|
||||
{ source: 'admin-dsms', target: 'admin-compliance-workflow' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG FLOW ===
|
||||
{ source: 'admin-rag', target: 'admin-quality' },
|
||||
{ source: 'admin-rag', target: 'admin-agents' },
|
||||
{ source: 'admin-ocr-labeling', target: 'admin-magic-help', label: 'Training' },
|
||||
{ source: 'admin-magic-help', target: 'admin-klausur-korrektur', label: 'Korrektur' },
|
||||
{ source: 'admin-quality', target: 'admin-test-quality' },
|
||||
{ source: 'admin-agents', target: 'admin-test-quality', label: 'BQAS' },
|
||||
{ source: 'admin-klausur-korrektur', target: 'admin-quality', label: 'Audit' },
|
||||
|
||||
// === INFRASTRUKTUR FLOW ===
|
||||
{ source: 'admin-security', target: 'admin-sbom', label: 'Dependencies' },
|
||||
{ source: 'admin-sbom', target: 'admin-tests' },
|
||||
{ source: 'admin-tests', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-cicd', target: 'admin-middleware' },
|
||||
{ source: 'admin-middleware', target: 'admin-gpu', label: 'GPU' },
|
||||
{ source: 'admin-security', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
|
||||
// === BILDUNG FLOW ===
|
||||
{ source: 'admin-edu-search', target: 'admin-rag', label: 'Quellen' },
|
||||
{ source: 'admin-edu-search', target: 'admin-zeugnisse' },
|
||||
{ source: 'admin-training', target: 'admin-onboarding' },
|
||||
{ source: 'admin-foerderantrag', target: 'admin-docs', label: 'Docs' },
|
||||
|
||||
// === KOMMUNIKATION FLOW ===
|
||||
{ source: 'admin-video', target: 'admin-matrix', label: 'Voice' },
|
||||
{ source: 'admin-mail', target: 'admin-alerts' },
|
||||
{ source: 'admin-alerts', target: 'admin-mail', label: 'Inbox' },
|
||||
|
||||
// === ENTWICKLUNG FLOW ===
|
||||
{ source: 'admin-workflow', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-workflow', target: 'admin-docs' },
|
||||
{ source: 'admin-game', target: 'admin-unity', label: 'Editor' },
|
||||
{ source: 'admin-companion', target: 'admin-agents', label: 'Agents' },
|
||||
{ source: 'admin-brandbook', target: 'admin-screen-flow' },
|
||||
{ source: 'admin-docs', target: 'admin-architecture' },
|
||||
{ source: 'admin-content', target: 'admin-brandbook' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// CATEGORY COLORS
|
||||
// ============================================
|
||||
|
||||
const STUDIO_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
navigation: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
|
||||
content: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
|
||||
communication: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
|
||||
school: { bg: '#fce7f3', border: '#ec4899', text: '#9d174d' },
|
||||
admin: { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
|
||||
ai: { bg: '#cffafe', border: '#06b6d4', text: '#0e7490' },
|
||||
}
|
||||
|
||||
// Colors from navigation.ts
|
||||
const ADMIN_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
overview: { bg: '#e0f2fe', border: '#0ea5e9', text: '#0369a1' }, // Sky (Meta)
|
||||
dsgvo: { bg: '#ede9fe', border: '#7c3aed', text: '#5b21b6' }, // Violet
|
||||
compliance: { bg: '#f3e8ff', border: '#9333ea', text: '#6b21a8' }, // Purple
|
||||
ai: { bg: '#ccfbf1', border: '#14b8a6', text: '#0f766e' }, // Teal
|
||||
infrastructure: { bg: '#ffedd5', border: '#f97316', text: '#c2410c' },// Orange
|
||||
education: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' }, // Blue
|
||||
communication: { bg: '#dcfce7', border: '#22c55e', text: '#166534' }, // Green
|
||||
development: { bg: '#f1f5f9', border: '#64748b', text: '#334155' }, // Slate
|
||||
}
|
||||
|
||||
const STUDIO_LABELS: Record<string, string> = {
|
||||
navigation: 'Navigation',
|
||||
content: 'Content & Tools',
|
||||
communication: 'Kommunikation',
|
||||
school: 'Schulverwaltung',
|
||||
admin: 'Administration',
|
||||
ai: 'KI & Assistent',
|
||||
}
|
||||
|
||||
// Labels from navigation.ts
|
||||
const ADMIN_LABELS: Record<string, string> = {
|
||||
overview: 'Uebersicht & Meta',
|
||||
dsgvo: 'DSGVO',
|
||||
compliance: 'Compliance & GRC',
|
||||
ai: 'KI & Automatisierung',
|
||||
infrastructure: 'Infrastruktur & DevOps',
|
||||
education: 'Bildung & Schule',
|
||||
communication: 'Kommunikation & Alerts',
|
||||
development: 'Entwicklung & Produkte',
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER: Find all connected nodes (recursive)
|
||||
// ============================================
|
||||
|
||||
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 getNodePosition = (
|
||||
id: string,
|
||||
category: string,
|
||||
screens: ScreenDefinition[],
|
||||
flowType: FlowType
|
||||
) => {
|
||||
const studioPositions: Record<string, { x: number; y: number }> = {
|
||||
navigation: { x: 400, y: 50 },
|
||||
content: { x: 50, y: 250 },
|
||||
communication: { x: 750, y: 250 },
|
||||
school: { x: 50, y: 500 },
|
||||
admin: { x: 750, y: 500 },
|
||||
ai: { x: 400, y: 380 },
|
||||
}
|
||||
|
||||
const adminPositions: Record<string, { x: number; y: number }> = {
|
||||
overview: { x: 400, y: 30 },
|
||||
dsgvo: { x: 50, y: 150 },
|
||||
compliance: { x: 700, y: 150 },
|
||||
ai: { x: 50, y: 350 },
|
||||
communication: { x: 400, y: 350 },
|
||||
infrastructure: { x: 700, y: 350 },
|
||||
education: { x: 50, y: 550 },
|
||||
development: { x: 400, y: 550 },
|
||||
}
|
||||
|
||||
const positions = flowType === 'studio' ? studioPositions : adminPositions
|
||||
const base = 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
|
||||
// ============================================
|
||||
import { useCallback } from 'react'
|
||||
import { useScreenFlow } from './useScreenFlow'
|
||||
import {
|
||||
FlowTypeSelector,
|
||||
StatsBar,
|
||||
CategoryFilter,
|
||||
ConnectedScreensList,
|
||||
FlowDiagram,
|
||||
ScreenList,
|
||||
} from './_components'
|
||||
import type { ScreenDefinition } from './types'
|
||||
|
||||
export default function ScreenFlowPage() {
|
||||
const [flowType, setFlowType] = useState<FlowType>('admin')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||||
const [previewScreen, setPreviewScreen] = useState<ScreenDefinition | null>(null)
|
||||
const {
|
||||
flowType,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
previewScreen,
|
||||
setPreviewScreen,
|
||||
screens,
|
||||
colors,
|
||||
labels,
|
||||
baseUrl,
|
||||
nodes,
|
||||
edges,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
handleFlowTypeChange,
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
stats,
|
||||
categories,
|
||||
connectedScreens,
|
||||
} = useScreenFlow()
|
||||
|
||||
// Get data based on flow type
|
||||
const screens = flowType === 'studio' ? STUDIO_SCREENS : ADMIN_SCREENS
|
||||
const connections = flowType === 'studio' ? STUDIO_CONNECTIONS : ADMIN_CONNECTIONS
|
||||
const colors = flowType === 'studio' ? STUDIO_COLORS : ADMIN_COLORS
|
||||
const labels = flowType === 'studio' ? STUDIO_LABELS : ADMIN_LABELS
|
||||
const baseUrl = flowType === 'studio' ? 'http://macmini:8000' : 'http://macmini:3002'
|
||||
|
||||
// Calculate connected nodes
|
||||
const connectedNodes = useMemo(() => {
|
||||
if (!selectedNode) return new Set<string>()
|
||||
return findConnectedNodes(selectedNode, connections, 'children')
|
||||
}, [selectedNode, connections])
|
||||
|
||||
// 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, screens, flowType)
|
||||
|
||||
// Determine opacity
|
||||
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',
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [screens, colors, flowType, 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 },
|
||||
}
|
||||
})
|
||||
}, [connections, 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])
|
||||
|
||||
// Reset when flow type changes
|
||||
const handleFlowTypeChange = useCallback((newType: FlowType) => {
|
||||
setFlowType(newType)
|
||||
setSelectedNode(null)
|
||||
setSelectedCategory(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
|
||||
// Handle node click
|
||||
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
||||
const screen = screens.find(s => s.id === node.id)
|
||||
|
||||
if (selectedNode === node.id) {
|
||||
// Double-click: open in new tab
|
||||
if (screen?.url) {
|
||||
window.open(`${baseUrl}${screen.url}`, '_blank')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedNode(node.id)
|
||||
setSelectedCategory(null)
|
||||
|
||||
if (screen) {
|
||||
setPreviewScreen(screen)
|
||||
}
|
||||
}, [screens, baseUrl, selectedNode])
|
||||
|
||||
// Handle background click - deselect
|
||||
const onPaneClick = useCallback(() => {
|
||||
const handleReset = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
}, [setSelectedNode, setPreviewScreen])
|
||||
|
||||
// Stats
|
||||
const stats = {
|
||||
totalScreens: screens.length,
|
||||
totalConnections: connections.length,
|
||||
connectedCount: connectedNodes.size,
|
||||
}
|
||||
const handleCategoryReset = useCallback(() => {
|
||||
setSelectedCategory(null)
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}, [setSelectedCategory, setSelectedNode, setPreviewScreen])
|
||||
|
||||
const categories = Object.keys(labels)
|
||||
const handleSelectCategory = useCallback((category: string | null) => {
|
||||
setSelectedCategory(category)
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}, [setSelectedCategory, setSelectedNode, setPreviewScreen])
|
||||
|
||||
// Connected screens list
|
||||
const connectedScreens = selectedNode
|
||||
? screens.filter(s => connectedNodes.has(s.id))
|
||||
: []
|
||||
const handleSelectScreen = useCallback((screen: ScreenDefinition) => {
|
||||
setSelectedNode(screen.id)
|
||||
setSelectedCategory(null)
|
||||
setPreviewScreen(screen)
|
||||
}, [setSelectedNode, setSelectedCategory, setPreviewScreen])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Flow Type Selector */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => handleFlowTypeChange('studio')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'studio'
|
||||
? 'border-green-500 bg-green-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'studio' ? 'bg-green-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
🎓
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Studio (Port 8000)</div>
|
||||
<div className="text-sm text-slate-500">Lehrer-Oberflaeche</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{STUDIO_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<FlowTypeSelector
|
||||
flowType={flowType}
|
||||
onFlowTypeChange={handleFlowTypeChange}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => handleFlowTypeChange('admin')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'admin'
|
||||
? 'border-primary-500 bg-primary-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'admin' ? 'bg-primary-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
⚙️
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Admin v2 (Port 3002)</div>
|
||||
<div className="text-sm text-slate-500">Admin Panel</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{ADMIN_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<StatsBar
|
||||
totalScreens={stats.totalScreens}
|
||||
totalConnections={stats.totalConnections}
|
||||
connectedCount={stats.connectedCount}
|
||||
selectedNode={selectedNode}
|
||||
previewScreen={previewScreen}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
|
||||
{/* Stats & Selection Info */}
|
||||
<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-primary-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>
|
||||
<CategoryFilter
|
||||
screens={screens}
|
||||
categories={categories}
|
||||
colors={colors}
|
||||
labels={labels}
|
||||
selectedCategory={selectedCategory}
|
||||
selectedNode={selectedNode}
|
||||
onSelectCategory={handleSelectCategory}
|
||||
onReset={handleCategoryReset}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<ConnectedScreensList
|
||||
selectedNode={selectedNode}
|
||||
connectedScreens={connectedScreens}
|
||||
colors={colors}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
|
||||
{/* 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-primary-500' : ''
|
||||
}`}
|
||||
style={{
|
||||
background: isCurrentNode ? catColors.border : catColors.bg,
|
||||
color: isCurrentNode ? 'white' : catColors.text,
|
||||
}}
|
||||
>
|
||||
<span>{screen.icon}</span>
|
||||
{screen.name}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FlowDiagram
|
||||
flowType={flowType}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
screens={screens}
|
||||
colors={colors}
|
||||
labels={labels}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
{/* 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">
|
||||
{flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin v2'}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{categories.slice(0, 4).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>
|
||||
<ScreenList
|
||||
screens={screens}
|
||||
colors={colors}
|
||||
labels={labels}
|
||||
baseUrl={baseUrl}
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectScreen={handleSelectScreen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
20
admin-lehrer/app/(admin)/development/screen-flow/types.ts
Normal file
20
admin-lehrer/app/(admin)/development/screen-flow/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Screen Flow - Type definitions
|
||||
*/
|
||||
|
||||
export interface ScreenDefinition {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
icon: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface ConnectionDef {
|
||||
source: string
|
||||
target: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type FlowType = 'studio' | 'admin'
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Screen Flow - State management hook
|
||||
*/
|
||||
|
||||
import { useCallback, useState, useMemo, useEffect } from 'react'
|
||||
import { Node, Edge, useNodesState, useEdgesState, MarkerType } from 'reactflow'
|
||||
|
||||
import type { ScreenDefinition, FlowType } from './types'
|
||||
import {
|
||||
STUDIO_SCREENS, ADMIN_SCREENS,
|
||||
STUDIO_CONNECTIONS, ADMIN_CONNECTIONS,
|
||||
STUDIO_COLORS, ADMIN_COLORS,
|
||||
STUDIO_LABELS, ADMIN_LABELS,
|
||||
} from './data'
|
||||
import { findConnectedNodes, getNodePosition } from './helpers'
|
||||
|
||||
export function useScreenFlow() {
|
||||
const [flowType, setFlowType] = useState<FlowType>('admin')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||||
const [previewScreen, setPreviewScreen] = useState<ScreenDefinition | null>(null)
|
||||
|
||||
// Derived data based on flow type
|
||||
const screens = flowType === 'studio' ? STUDIO_SCREENS : ADMIN_SCREENS
|
||||
const connections = flowType === 'studio' ? STUDIO_CONNECTIONS : ADMIN_CONNECTIONS
|
||||
const colors = flowType === 'studio' ? STUDIO_COLORS : ADMIN_COLORS
|
||||
const labels = flowType === 'studio' ? STUDIO_LABELS : ADMIN_LABELS
|
||||
const baseUrl = flowType === 'studio' ? 'http://macmini:8000' : 'http://macmini:3002'
|
||||
|
||||
// BFS connected nodes
|
||||
const connectedNodes = useMemo(() => {
|
||||
if (!selectedNode) return new Set<string>()
|
||||
return findConnectedNodes(selectedNode, connections, 'children')
|
||||
}, [selectedNode, connections])
|
||||
|
||||
// Build ReactFlow nodes
|
||||
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, screens, flowType)
|
||||
|
||||
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',
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [screens, colors, flowType, selectedCategory, selectedNode, connectedNodes])
|
||||
|
||||
// Build ReactFlow edges
|
||||
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 },
|
||||
}
|
||||
})
|
||||
}, [connections, selectedNode, connectedNodes])
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
|
||||
// Sync memoized nodes/edges into ReactFlow state
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes)
|
||||
setEdges(initialEdges)
|
||||
}, [initialNodes, initialEdges, setNodes, setEdges])
|
||||
|
||||
// Reset on flow type change
|
||||
const handleFlowTypeChange = useCallback((newType: FlowType) => {
|
||||
setFlowType(newType)
|
||||
setSelectedNode(null)
|
||||
setSelectedCategory(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
|
||||
// Node click: select or open on double-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)
|
||||
}
|
||||
}, [screens, baseUrl, selectedNode])
|
||||
|
||||
// Background click: deselect
|
||||
const onPaneClick = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
|
||||
const stats = {
|
||||
totalScreens: screens.length,
|
||||
totalConnections: connections.length,
|
||||
connectedCount: connectedNodes.size,
|
||||
}
|
||||
|
||||
const categories = Object.keys(labels)
|
||||
|
||||
const connectedScreens = selectedNode
|
||||
? screens.filter(s => connectedNodes.has(s.id))
|
||||
: []
|
||||
|
||||
return {
|
||||
flowType,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
previewScreen,
|
||||
setPreviewScreen,
|
||||
screens,
|
||||
colors,
|
||||
labels,
|
||||
baseUrl,
|
||||
connectedNodes,
|
||||
nodes,
|
||||
edges,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
handleFlowTypeChange,
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
stats,
|
||||
categories,
|
||||
connectedScreens,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user