feat(ui): Dark-Glassmorphism-Designsystem und Grundlayout

Optischer Rahmen der Anwendung:

- Hintergrund aus drei langsam driftenden Nebeln, feinem Raster und einer
  Rauschtextur gegen Farbbanding
- .glass / .glass-soft als Grundflaechen, .card-glow fuer den Hover-Effekt
  mit Farbverlaufsrand je Projektakzent
- Buttons, Formularfelder und Scrollbars im gleichen Stil
- prefers-reduced-motion schaltet saemtliche Animationen ab

Dazu Kopfzeile mit aktiver Navigationsmarkierung, ein wiederverwendbarer
Modal-Dialog (Escape schliesst, Scroll wird gesperrt), das SVG-Icon sowie
Fehler- und 404-Seiten. Die Fehlerseite erkennt Datenbankprobleme und nennt
direkt die drei Befehle, die in dem Fall meistens helfen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 19:34:15 +02:00
co-authored by Claude Opus 5
parent 7690bc87c5
commit 6fcbc04110
7 changed files with 642 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
"use client";
import { useEffect } from "react";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
interface ModalProps {
open: boolean;
title: string;
description?: string;
onClose: () => void;
children: React.ReactNode;
footer?: React.ReactNode;
width?: "md" | "lg" | "xl";
}
export function Modal({
open,
title,
description,
onClose,
children,
footer,
width = "lg",
}: ModalProps) {
useEffect(() => {
if (!open) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
document.addEventListener("keydown", onKey);
const previous = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = previous;
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 sm:p-8">
<button
type="button"
aria-label="Dialog schliessen"
onClick={onClose}
className="fixed inset-0 cursor-default bg-[#03040c]/80 backdrop-blur-sm"
/>
<div
role="dialog"
aria-modal="true"
aria-label={title}
className={cn(
"glass fade-up relative z-10 my-auto w-full p-6",
width === "md" && "max-w-lg",
width === "lg" && "max-w-2xl",
width === "xl" && "max-w-4xl",
)}
>
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-white">{title}</h2>
{description ? (
<p className="mt-1 text-sm text-slate-400">{description}</p>
) : null}
</div>
<button
type="button"
onClick={onClose}
className="rounded-lg p-1.5 text-slate-400 transition-colors hover:bg-white/8 hover:text-white"
aria-label="Schliessen"
>
<X className="h-4 w-4" />
</button>
</div>
{children}
{footer ? <div className="mt-6 flex justify-end gap-2">{footer}</div> : null}
</div>
</div>
);
}