feat: Dark Mode mit Toggle, localStorage-Persistenz und System-Preference

- Sun/Moon Toggle im Top-Bar
- Alle Seiten und Komponenten mit dark: Varianten
- Tailwind darkMode: 'class' Strategie
- Respektiert prefers-color-scheme als Default
This commit is contained in:
root 2026-03-20 23:51:06 +00:00
parent 9e3b431e7c
commit 3bad762329
18 changed files with 245 additions and 193 deletions

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Workflow Portal</title>
</head>
<body class="bg-gray-50 text-gray-900 antialiased">
<body class="bg-gray-50 text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@ -29,7 +29,7 @@ export default function ConfirmDialog({
<div className="flex flex-col items-center text-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full ${
variant === 'danger' ? 'bg-red-100' : 'bg-amber-100'
variant === 'danger' ? 'bg-red-100 dark:bg-red-900/40' : 'bg-amber-100 dark:bg-amber-900/40'
}`}
>
<AlertTriangle
@ -37,8 +37,8 @@ export default function ConfirmDialog({
size={24}
/>
</div>
<h3 className="mt-3 text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-2 text-sm text-gray-500">{message}</p>
<h3 className="mt-3 text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{message}</p>
<div className="mt-6 flex w-full gap-3">
<button
onClick={onClose}

View File

@ -15,12 +15,12 @@ export default function EmptyState({
}: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
<Icon className="text-gray-400" size={32} />
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700">
<Icon className="text-gray-400 dark:text-gray-500" size={32} />
</div>
<h3 className="mt-4 text-base font-semibold text-gray-900">{title}</h3>
<h3 className="mt-4 text-base font-semibold text-gray-900 dark:text-white">{title}</h3>
{description && (
<p className="mt-1 text-sm text-gray-500">{description}</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
)}
{action && <div className="mt-6">{action}</div>}
</div>

View File

@ -6,13 +6,16 @@ import {
LayoutDashboard,
LogOut,
Menu,
Moon,
Settings,
Sun,
User,
Users,
X,
} from 'lucide-react';
import { Menu as HeadlessMenu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
import { useAuth } from '../hooks/useAuth';
import { useDarkMode } from '../hooks/useDarkMode';
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
@ -29,6 +32,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const { user, logout } = useAuth();
const navigate = useNavigate();
const [sidebarOpen, setSidebarOpen] = useState(false);
const { isDark, toggle: toggleDark } = useDarkMode();
const isAdmin =
user?.role === 'SYSTEM_ADMIN' || user?.role === 'TECHNICIAN';
@ -41,22 +45,22 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
isActive
? 'bg-primary-50 text-primary-700'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'
}`;
const sidebarContent = (
<>
<div className="flex h-16 items-center gap-3 border-b border-gray-200 px-6">
<div className="flex h-16 items-center gap-3 border-b border-gray-200 px-6 dark:border-gray-700">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-600 text-sm font-bold text-white">
WP
</div>
<div>
<h1 className="text-sm font-semibold text-gray-900">
<h1 className="text-sm font-semibold text-gray-900 dark:text-white">
Workflow Portal
</h1>
{user?.tenant && (
<p className="text-xs text-gray-500">{user.tenant.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{user.tenant.name}</p>
)}
</div>
</div>
@ -77,8 +81,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{isAdmin && (
<>
<div className="my-4 border-t border-gray-200" />
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
Administration
</p>
{adminNavigation.map((item) => (
@ -96,17 +100,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
)}
</nav>
<div className="border-t border-gray-200 p-4">
<div className="border-t border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-400">
{user?.firstName?.[0]}
{user?.lastName?.[0]}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
<p className="truncate text-sm font-medium text-gray-900 dark:text-white">
{user?.firstName} {user?.lastName}
</p>
<p className="truncate text-xs text-gray-500">{user?.email}</p>
<p className="truncate text-xs text-gray-500 dark:text-gray-400">{user?.email}</p>
</div>
</div>
</div>
@ -114,7 +118,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
);
return (
<div className="flex h-screen overflow-hidden bg-gray-50">
<div className="flex h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
@ -125,13 +129,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{/* Mobile sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-white shadow-xl transition-transform duration-200 lg:hidden ${
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col bg-white shadow-xl transition-transform duration-200 lg:hidden dark:bg-gray-900 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<button
onClick={() => setSidebarOpen(false)}
className="absolute right-3 top-4 rounded-lg p-1 text-gray-400 hover:bg-gray-100"
className="absolute right-3 top-4 rounded-lg p-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
>
<X size={20} />
</button>
@ -139,25 +143,34 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</aside>
{/* Desktop sidebar */}
<aside className="hidden w-64 flex-col border-r border-gray-200 bg-white lg:flex">
<aside className="hidden w-64 flex-col border-r border-gray-200 bg-white lg:flex dark:border-gray-700 dark:bg-gray-900">
{sidebarContent}
</aside>
{/* Main content */}
<div className="flex flex-1 flex-col overflow-hidden">
{/* Top bar */}
<header className="flex h-16 items-center justify-between border-b border-gray-200 bg-white px-4 lg:px-8">
<header className="flex h-16 items-center justify-between border-b border-gray-200 bg-white px-4 lg:px-8 dark:border-gray-700 dark:bg-gray-800">
<button
onClick={() => setSidebarOpen(true)}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 lg:hidden"
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 lg:hidden dark:text-gray-400 dark:hover:bg-gray-700"
>
<Menu size={22} />
</button>
<div className="flex-1" />
{/* Dark mode toggle */}
<button
onClick={toggleDark}
className="mr-2 rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
title={isDark ? 'Helles Design' : 'Dunkles Design'}
>
{isDark ? <Sun size={20} /> : <Moon size={20} />}
</button>
<HeadlessMenu as="div" className="relative">
<MenuButton className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">
<MenuButton className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">
<User size={18} />
<span className="hidden sm:inline">
{user?.firstName} {user?.lastName}
@ -172,13 +185,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<MenuItems className="absolute right-0 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none">
<MenuItems className="absolute right-0 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none dark:border-gray-700 dark:bg-gray-800">
<MenuItem>
{({ focus }) => (
<Link
to="/"
className={`flex items-center gap-2 px-4 py-2 text-sm ${
focus ? 'bg-gray-50 text-gray-900' : 'text-gray-700'
focus ? 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white' : 'text-gray-700 dark:text-gray-200'
}`}
>
<User size={16} />
@ -191,7 +204,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<button
onClick={handleLogout}
className={`flex w-full items-center gap-2 px-4 py-2 text-sm ${
focus ? 'bg-gray-50 text-gray-900' : 'text-gray-700'
focus ? 'bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white' : 'text-gray-700 dark:text-gray-200'
}`}
>
<LogOut size={16} />

View File

@ -51,17 +51,17 @@ export default function Modal({
leaveTo="opacity-0 scale-95"
>
<DialogPanel
className={`w-full ${sizeClasses[size]} transform rounded-xl bg-white p-6 shadow-xl transition-all`}
className={`w-full ${sizeClasses[size]} transform rounded-xl bg-white p-6 shadow-xl transition-all dark:bg-gray-800`}
>
<div className="flex items-center justify-between">
{title && (
<DialogTitle className="text-lg font-semibold text-gray-900">
<DialogTitle className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</DialogTitle>
)}
<button
onClick={onClose}
className="ml-auto rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
className="ml-auto rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
>
<X size={20} />
</button>

View File

@ -27,13 +27,13 @@ export default function Pagination({
<button
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-400 dark:hover:bg-gray-700"
>
<ChevronLeft size={18} />
</button>
{pages.map((p, idx) =>
p === '...' ? (
<span key={`dots-${idx}`} className="px-2 text-gray-400">
<span key={`dots-${idx}`} className="px-2 text-gray-400 dark:text-gray-500">
...
</span>
) : (
@ -43,7 +43,7 @@ export default function Pagination({
className={`min-w-[36px] rounded-lg px-3 py-1.5 text-sm font-medium ${
p === page
? 'bg-primary-600 text-white'
: 'text-gray-600 hover:bg-gray-100'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700'
}`}
>
{p}
@ -53,7 +53,7 @@ export default function Pagination({
<button
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-400 dark:hover:bg-gray-700"
>
<ChevronRight size={18} />
</button>

View File

@ -35,7 +35,7 @@ export default function SearchBar({
return (
<div className="relative">
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
size={18}
/>
<input
@ -48,7 +48,7 @@ export default function SearchBar({
{internal && (
<button
onClick={() => handleChange('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
>
<X size={16} />
</button>

View File

@ -6,19 +6,19 @@ const statusConfig: Record<
> = {
DRAFT: {
label: 'Entwurf',
className: 'bg-gray-100 text-gray-700 ring-gray-300',
className: 'bg-gray-100 text-gray-700 ring-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:ring-gray-600',
},
REVIEW: {
label: 'In Prüfung',
className: 'bg-yellow-50 text-yellow-700 ring-yellow-300',
className: 'bg-yellow-50 text-yellow-700 ring-yellow-300 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-700',
},
PUBLISHED: {
label: 'Veröffentlicht',
className: 'bg-green-50 text-green-700 ring-green-300',
className: 'bg-green-50 text-green-700 ring-green-300 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-700',
},
ARCHIVED: {
label: 'Archiviert',
className: 'bg-red-50 text-red-700 ring-red-300',
className: 'bg-red-50 text-red-700 ring-red-300 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-700',
},
};

View File

@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
function getInitialTheme(): boolean {
const stored = localStorage.getItem('theme');
if (stored === 'dark') return true;
if (stored === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
export function useDarkMode() {
const [isDark, setIsDark] = useState(getInitialTheme);
useEffect(() => {
const root = document.documentElement;
if (isDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}, [isDark]);
// Listen for system preference changes when no explicit choice stored
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
if (!localStorage.getItem('theme')) {
setIsDark(e.matches);
}
};
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
const toggle = () => setIsDark((prev) => !prev);
return { isDark, toggle };
}

View File

@ -7,24 +7,24 @@
@apply scroll-smooth;
}
body {
@apply min-h-screen bg-gray-50 text-gray-900;
@apply min-h-screen bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100;
}
}
@layer components {
.btn-primary {
@apply inline-flex items-center justify-center rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-gray-900;
}
.btn-secondary {
@apply inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-900;
}
.btn-danger {
@apply inline-flex items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-gray-900;
}
.input-field {
@apply block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500;
@apply block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400;
}
.card {
@apply rounded-xl border border-gray-200 bg-white shadow-sm;
@apply rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800;
}
}

View File

@ -77,25 +77,25 @@ export default function Dashboard() {
label: 'Gesamt',
value: stats.total,
icon: FileText,
color: 'bg-primary-50 text-primary-600',
color: 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400',
},
{
label: 'Veröffentlicht',
value: stats.published,
icon: FileCheck,
color: 'bg-green-50 text-green-600',
color: 'bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400',
},
{
label: 'In Prüfung',
value: stats.review,
icon: Clock,
color: 'bg-yellow-50 text-yellow-600',
color: 'bg-yellow-50 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400',
},
{
label: 'Entwurf',
value: stats.draft,
icon: Archive,
color: 'bg-gray-100 text-gray-600',
color: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
},
];
@ -104,10 +104,10 @@ export default function Dashboard() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Willkommen, {user?.firstName}!
</h1>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Hier ist eine Übersicht Ihrer Workflows.
</p>
</div>
@ -139,10 +139,10 @@ export default function Dashboard() {
</div>
</div>
<div className="mt-3">
<p className="text-2xl font-bold text-gray-900">
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{loading ? '-' : card.value}
</p>
<p className="text-sm text-gray-500">{card.label}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{card.label}</p>
</div>
</div>
))}
@ -154,12 +154,12 @@ export default function Dashboard() {
to="/workflows/new"
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary-50">
<Plus className="text-primary-600" size={24} />
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary-50 dark:bg-primary-900/30">
<Plus className="text-primary-600 dark:text-primary-400" size={24} />
</div>
<div>
<h3 className="font-semibold text-gray-900">Neuer Workflow</h3>
<p className="text-sm text-gray-500">
<h3 className="font-semibold text-gray-900 dark:text-white">Neuer Workflow</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Erstellen Sie einen neuen Workflow
</p>
</div>
@ -168,12 +168,12 @@ export default function Dashboard() {
to="/workflows?template=true"
className="card flex items-center gap-4 p-5 transition-shadow hover:shadow-md"
>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-50">
<Layers className="text-purple-600" size={24} />
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-50 dark:bg-purple-900/30">
<Layers className="text-purple-600 dark:text-purple-400" size={24} />
</div>
<div>
<h3 className="font-semibold text-gray-900">Vorlagen durchsuchen</h3>
<p className="text-sm text-gray-500">
<h3 className="font-semibold text-gray-900 dark:text-white">Vorlagen durchsuchen</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Starten Sie mit einer bestehenden Vorlage
</p>
</div>
@ -182,13 +182,13 @@ export default function Dashboard() {
{/* Recent workflows */}
<div className="card">
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<h2 className="text-base font-semibold text-gray-900">
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700">
<h2 className="text-base font-semibold text-gray-900 dark:text-white">
Aktuelle Workflows
</h2>
<Link
to="/workflows"
className="text-sm font-medium text-primary-600 hover:text-primary-700"
className="text-sm font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
Alle anzeigen
</Link>
@ -198,34 +198,34 @@ export default function Dashboard() {
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary-600 border-t-transparent" />
</div>
) : recent.length === 0 ? (
<div className="py-12 text-center text-sm text-gray-500">
<div className="py-12 text-center text-sm text-gray-500 dark:text-gray-400">
Noch keine Workflows vorhanden.
</div>
) : (
<ul className="divide-y divide-gray-100">
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{recent.map((wf) => (
<li key={wf.id}>
<Link
to={`/workflows/${wf.id}`}
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50"
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
<FilePlus className="text-gray-500" size={20} />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
<FilePlus className="text-gray-500 dark:text-gray-400" size={20} />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900">
<p className="truncate font-medium text-gray-900 dark:text-white">
{wf.title}
</p>
<p className="truncate text-sm text-gray-500">
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
{wf.tenant && (
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
<span className="font-medium text-primary-600 dark:text-primary-400">{wf.tenant.name}</span>
)}
{wf.tenant && <span> &middot; </span>}
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte &middot; v{wf.version}
</p>
</div>
<StatusBadge status={wf.status} />
<span className="hidden text-xs text-gray-400 sm:inline">
<span className="hidden text-xs text-gray-400 dark:text-gray-500 sm:inline">
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
</span>
</Link>

View File

@ -67,24 +67,24 @@ export default function LoginPage() {
</div>
{/* Right login form */}
<div className="flex flex-1 items-center justify-center px-6 py-12">
<div className="flex flex-1 items-center justify-center bg-white px-6 py-12 dark:bg-gray-900">
<div className="w-full max-w-sm">
<div className="mb-8 text-center lg:hidden">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-primary-600 text-xl font-bold text-white">
WP
</div>
<h1 className="mt-3 text-xl font-bold text-gray-900">
<h1 className="mt-3 text-xl font-bold text-gray-900 dark:text-white">
Workflow Portal
</h1>
</div>
<h2 className="text-2xl font-bold text-gray-900">Anmelden</h2>
<p className="mt-2 text-sm text-gray-500">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Anmelden</h2>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Melden Sie sich mit Ihren Zugangsdaten an.
</p>
{error && (
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400">
{error}
</div>
)}
@ -93,7 +93,7 @@ export default function LoginPage() {
<div>
<label
htmlFor="email"
className="mb-1.5 block text-sm font-medium text-gray-700"
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
E-Mail-Adresse
</label>
@ -112,7 +112,7 @@ export default function LoginPage() {
<div>
<label
htmlFor="password"
className="mb-1.5 block text-sm font-medium text-gray-700"
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Passwort
</label>
@ -130,7 +130,7 @@ export default function LoginPage() {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>

View File

@ -129,8 +129,8 @@ export default function TenantList() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Mandanten</h1>
<p className="mt-1 text-sm text-gray-500">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Mandanten</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Verwalten Sie die Mandanten des Portals.
</p>
</div>
@ -172,47 +172,47 @@ export default function TenantList() {
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Slug
</th>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Aktionen
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
<tbody className="divide-y divide-gray-100 bg-white dark:divide-gray-700 dark:bg-gray-800">
{tenants.map((tenant) => (
<tr key={tenant.id} className="hover:bg-gray-50">
<tr key={tenant.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="whitespace-nowrap px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-50 text-sm font-bold text-primary-700">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-50 text-sm font-bold text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
{tenant.name[0]}
</div>
<span className="font-medium text-gray-900">
<span className="font-medium text-gray-900 dark:text-white">
{tenant.name}
</span>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{tenant.slug}
</td>
<td className="whitespace-nowrap px-6 py-4">
{tenant.active ? (
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-300">
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-300 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-700">
<Check size={12} />
Aktiv
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300">
<span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:ring-gray-600">
<X size={12} />
Inaktiv
</span>
@ -222,14 +222,14 @@ export default function TenantList() {
<div className="flex items-center justify-end gap-2">
<button
onClick={() => openEdit(tenant)}
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
title="Bearbeiten"
>
<Edit size={16} />
</button>
<button
onClick={() => setDeactivateTarget(tenant)}
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
title={tenant.active ? 'Deaktivieren' : 'Aktivieren'}
>
<Power size={16} />
@ -256,7 +256,7 @@ export default function TenantList() {
>
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
<input
@ -278,7 +278,7 @@ export default function TenantList() {
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Slug *
</label>
<input
@ -294,9 +294,9 @@ export default function TenantList() {
type="checkbox"
checked={formActive}
onChange={(e) => setFormActive(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
/>
<span className="text-sm text-gray-700">Aktiv</span>
<span className="text-sm text-gray-700 dark:text-gray-300">Aktiv</span>
</label>
<div className="flex justify-end gap-3 pt-2">
<button

View File

@ -161,8 +161,8 @@ export default function UserList() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Benutzer</h1>
<p className="mt-1 text-sm text-gray-500">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Benutzer</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Verwalten Sie die Benutzer und deren Rollen.
</p>
</div>
@ -236,63 +236,63 @@ export default function UserList() {
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
E-Mail
</th>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Rolle
</th>
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Mandant
</th>
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500">
<th className="px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Aktionen
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
<tbody className="divide-y divide-gray-100 bg-white dark:divide-gray-700 dark:bg-gray-800">
{users.map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="whitespace-nowrap px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary-100 text-sm font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-400">
{u.firstName[0]}
{u.lastName[0]}
</div>
<span className="font-medium text-gray-900">
<span className="font-medium text-gray-900 dark:text-white">
{u.firstName} {u.lastName}
</span>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{u.email}
</td>
<td className="whitespace-nowrap px-6 py-4">
<span className="inline-flex rounded-full bg-primary-50 px-2.5 py-0.5 text-xs font-medium text-primary-700">
<span className="inline-flex rounded-full bg-primary-50 px-2.5 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
{roleLabels[u.role]}
</span>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{u.tenant?.name || '-'}
</td>
<td className="whitespace-nowrap px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => openEdit(u)}
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
title="Bearbeiten"
>
<Edit size={16} />
</button>
<button
onClick={() => setDeactivateTarget(u)}
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-red-500"
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700"
title="Deaktivieren"
>
<Power size={16} />
@ -321,7 +321,7 @@ export default function UserList() {
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Vorname *
</label>
<input
@ -333,7 +333,7 @@ export default function UserList() {
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Nachname *
</label>
<input
@ -346,7 +346,7 @@ export default function UserList() {
</div>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
E-Mail *
</label>
<input
@ -358,7 +358,7 @@ export default function UserList() {
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Passwort {editingUser ? '(leer lassen um beizubehalten)' : '*'}
</label>
<input
@ -371,7 +371,7 @@ export default function UserList() {
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Rolle *
</label>
<select
@ -387,7 +387,7 @@ export default function UserList() {
</select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Mandant
</label>
<select

View File

@ -280,24 +280,24 @@ export default function WorkflowEditor() {
<div className="flex items-center justify-between">
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<ArrowLeft size={16} />
Zurück
</button>
{autoSaved && (
<span className="text-xs text-green-600">Automatisch gespeichert</span>
<span className="text-xs text-green-600 dark:text-green-400">Automatisch gespeichert</span>
)}
</div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{isNew ? 'Neuer Workflow' : 'Workflow bearbeiten'}
</h1>
{/* Metadata */}
<div className="card space-y-5 p-6">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Titel *
</label>
<input
@ -310,7 +310,7 @@ export default function WorkflowEditor() {
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Beschreibung
</label>
<textarea
@ -324,7 +324,7 @@ export default function WorkflowEditor() {
{isAdmin && isNew && (
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Mandant *
</label>
<select
@ -344,7 +344,7 @@ export default function WorkflowEditor() {
<div className="grid gap-5 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Kategorie
</label>
<select
@ -362,7 +362,7 @@ export default function WorkflowEditor() {
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
Tags (kommagetrennt)
</label>
<input
@ -380,16 +380,16 @@ export default function WorkflowEditor() {
type="checkbox"
checked={isTemplate}
onChange={(e) => setIsTemplate(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
/>
<span className="text-sm text-gray-700">Als Vorlage speichern</span>
<span className="text-sm text-gray-700 dark:text-gray-300">Als Vorlage speichern</span>
</label>
</div>
{/* Steps */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Schritte</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schritte</h2>
<button onClick={addStep} className="btn-secondary gap-1.5 text-sm">
<Plus size={16} />
Schritt hinzufügen
@ -418,21 +418,21 @@ export default function WorkflowEditor() {
snapshot.isDragging ? 'shadow-lg ring-2 ring-primary-300' : ''
}`}
>
<div className="flex items-center gap-2 border-b border-gray-100 bg-gray-50 px-4 py-2">
<div className="flex items-center gap-2 border-b border-gray-100 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-700/50">
<div
{...provided.dragHandleProps}
className="cursor-grab text-gray-400 hover:text-gray-600"
className="cursor-grab text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<GripVertical size={18} />
</div>
<span className="text-sm font-medium text-gray-500">
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
Schritt {index + 1}
</span>
<StepTypeIcon type={step.type} />
<div className="flex-1" />
<button
onClick={() => removeStep(index)}
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-500"
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
>
<Trash2 size={16} />
</button>
@ -440,7 +440,7 @@ export default function WorkflowEditor() {
<div className="space-y-4 p-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1 block text-xs font-medium text-gray-500">
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
Titel
</label>
<input
@ -455,7 +455,7 @@ export default function WorkflowEditor() {
</div>
<div className="flex gap-4">
<div className="flex-1">
<label className="mb-1 block text-xs font-medium text-gray-500">
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
Typ
</label>
<select
@ -488,9 +488,9 @@ export default function WorkflowEditor() {
e.target.checked
)
}
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700"
/>
<span className="text-xs text-gray-600">
<span className="text-xs text-gray-600 dark:text-gray-400">
Pflicht
</span>
</label>
@ -498,7 +498,7 @@ export default function WorkflowEditor() {
</div>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-500">
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
Inhalt
</label>
<textarea
@ -532,7 +532,7 @@ export default function WorkflowEditor() {
{steps.length === 0 && (
<div className="card py-12 text-center">
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-400">
Noch keine Schritte vorhanden.
</p>
<button onClick={addStep} className="btn-primary mt-4 gap-1.5">

View File

@ -83,10 +83,10 @@ export default function WorkflowList() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{isTemplate ? 'Vorlagen' : 'Workflows'}
</h1>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{isTemplate
? 'Wiederverwendbare Workflow-Vorlagen'
: 'Alle Workflows Ihres Mandanten'}
@ -157,13 +157,13 @@ export default function WorkflowList() {
))}
</select>
)}
<div className="flex rounded-lg border border-gray-300 bg-white">
<div className="flex rounded-lg border border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-800">
<button
onClick={() => setViewMode('grid')}
className={`rounded-l-lg p-2 ${
viewMode === 'grid'
? 'bg-primary-50 text-primary-600'
: 'text-gray-400 hover:text-gray-600'
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
}`}
>
<Grid3X3 size={18} />
@ -172,8 +172,8 @@ export default function WorkflowList() {
onClick={() => setViewMode('list')}
className={`rounded-r-lg p-2 ${
viewMode === 'list'
? 'bg-primary-50 text-primary-600'
: 'text-gray-400 hover:text-gray-600'
? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
}`}
>
<List size={18} />
@ -212,23 +212,23 @@ export default function WorkflowList() {
className="card p-5 transition-shadow hover:shadow-md"
>
<div className="flex items-start justify-between gap-2">
<h3 className="line-clamp-1 font-semibold text-gray-900">
<h3 className="line-clamp-1 font-semibold text-gray-900 dark:text-white">
{wf.title}
</h3>
<StatusBadge status={wf.status} />
</div>
{wf.tenant && (
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-primary-600">
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400">
<Building2 size={12} />
{wf.tenant.name}
</div>
)}
{wf.description && (
<p className="mt-1 line-clamp-2 text-sm text-gray-500">
<p className="mt-1 line-clamp-2 text-sm text-gray-500 dark:text-gray-400">
{wf.description}
</p>
)}
<div className="mt-4 flex items-center justify-between text-xs text-gray-400">
<div className="mt-4 flex items-center justify-between text-xs text-gray-400 dark:text-gray-500">
<span>
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte &middot; v{wf.version}
</span>
@ -237,7 +237,7 @@ export default function WorkflowList() {
</span>
</div>
{wf.category && (
<div className="mt-3 flex items-center gap-1.5 text-xs text-gray-500">
<div className="mt-3 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
<Tag size={12} />
{wf.category.name}
</div>
@ -247,13 +247,13 @@ export default function WorkflowList() {
{wf.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600"
className="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{tag}
</span>
))}
{wf.tags.length > 3 && (
<span className="text-xs text-gray-400">
<span className="text-xs text-gray-400 dark:text-gray-500">
+{wf.tags.length - 3}
</span>
)}
@ -263,36 +263,36 @@ export default function WorkflowList() {
))}
</div>
) : (
<div className="card divide-y divide-gray-100">
<div className="card divide-y divide-gray-100 dark:divide-gray-700">
{workflows.map((wf) => (
<Link
key={wf.id}
to={`/workflows/${wf.id}`}
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50"
className="flex items-center gap-4 px-6 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
<FileText className="text-gray-500" size={20} />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
<FileText className="text-gray-500 dark:text-gray-400" size={20} />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900">{wf.title}</p>
<p className="truncate text-sm text-gray-500">
<p className="truncate font-medium text-gray-900 dark:text-white">{wf.title}</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
{wf.tenant && (
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
<span className="font-medium text-primary-600 dark:text-primary-400">{wf.tenant.name}</span>
)}
{wf.tenant && wf.description && <span> &middot; </span>}
{wf.description}
</p>
</div>
{wf.category && (
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline">
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline dark:bg-gray-700 dark:text-gray-300">
{wf.category.name}
</span>
)}
<span className="hidden text-sm text-gray-400 sm:inline">
<span className="hidden text-sm text-gray-400 dark:text-gray-500 sm:inline">
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte
</span>
<StatusBadge status={wf.status} />
<span className="hidden text-xs text-gray-400 lg:inline">
<span className="hidden text-xs text-gray-400 dark:text-gray-500 lg:inline">
{new Date(wf.updatedAt).toLocaleDateString('de-DE')}
</span>
</Link>

View File

@ -23,15 +23,15 @@ import ConfirmDialog from '../components/ConfirmDialog';
function getStepBgClass(type: WorkflowStep['type']): string {
switch (type) {
case 'WARNING':
return 'border-l-4 border-l-amber-400 bg-amber-50';
return 'border-l-4 border-l-amber-400 bg-amber-50 dark:bg-amber-900/20';
case 'COMMAND':
return 'border-l-4 border-l-purple-400 bg-gray-900';
return 'border-l-4 border-l-purple-400 bg-gray-900 dark:bg-gray-950';
case 'INFO':
return 'border-l-4 border-l-cyan-400 bg-cyan-50';
return 'border-l-4 border-l-cyan-400 bg-cyan-50 dark:bg-cyan-900/20';
case 'CHECKLIST':
return 'border-l-4 border-l-green-400 bg-green-50';
return 'border-l-4 border-l-green-400 bg-green-50 dark:bg-green-900/20';
default:
return 'border-l-4 border-l-primary-400 bg-white';
return 'border-l-4 border-l-primary-400 bg-white dark:bg-gray-800';
}
}
@ -115,7 +115,7 @@ export default function WorkflowView() {
{/* Back */}
<Link
to="/workflows"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<ArrowLeft size={16} />
Zurück zu Workflows
@ -126,15 +126,15 @@ export default function WorkflowView() {
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{workflow.title}
</h1>
<StatusBadge status={workflow.status} />
</div>
{workflow.description && (
<p className="mt-2 text-gray-600">{workflow.description}</p>
<p className="mt-2 text-gray-600 dark:text-gray-300">{workflow.description}</p>
)}
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-500">
<div className="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>Version {workflow.version}</span>
<span>{sortedSteps.length} Schritte</span>
{workflow.category && <span>{workflow.category.name}</span>}
@ -152,7 +152,7 @@ export default function WorkflowView() {
{workflow.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-primary-50 px-3 py-0.5 text-xs font-medium text-primary-700"
className="rounded-full bg-primary-50 px-3 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400"
>
{tag}
</span>
@ -181,7 +181,7 @@ export default function WorkflowView() {
{workflow.status !== 'ARCHIVED' && (
<button
onClick={() => setArchiveOpen(true)}
className="btn-secondary gap-1.5 text-red-600 hover:bg-red-50"
className="btn-secondary gap-1.5 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
>
<Archive size={16} />
Archivieren
@ -193,9 +193,9 @@ export default function WorkflowView() {
{/* Steps */}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Schritte</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schritte</h2>
{sortedSteps.length === 0 ? (
<div className="card py-12 text-center text-sm text-gray-500">
<div className="card py-12 text-center text-sm text-gray-500 dark:text-gray-400">
Keine Schritte vorhanden.
</div>
) : (
@ -208,21 +208,21 @@ export default function WorkflowView() {
<div className="p-5">
<div className="flex items-start gap-4">
{/* Step number */}
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white text-sm font-bold text-gray-700 shadow-sm ring-1 ring-gray-200">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white text-sm font-bold text-gray-700 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:ring-gray-600">
{idx + 1}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<StepTypeIcon type={step.type} showLabel />
{step.isRequired && (
<span className="rounded bg-red-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-red-600">
<span className="rounded bg-red-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-red-600 dark:bg-red-900/40 dark:text-red-400">
Pflicht
</span>
)}
</div>
<h3
className={`mt-1 font-semibold ${
step.type === 'COMMAND' ? 'text-gray-100' : 'text-gray-900'
step.type === 'COMMAND' ? 'text-gray-100' : 'text-gray-900 dark:text-white'
}`}
>
{step.title}
@ -235,10 +235,10 @@ export default function WorkflowView() {
<div
className={`mt-2 whitespace-pre-wrap text-sm ${
step.type === 'WARNING'
? 'text-amber-800'
? 'text-amber-800 dark:text-amber-300'
: step.type === 'INFO'
? 'text-cyan-800'
: 'text-gray-600'
? 'text-cyan-800 dark:text-cyan-300'
: 'text-gray-600 dark:text-gray-300'
}`}
>
{step.content}

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {