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
+56
View File
@@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { GitBranch, LayoutDashboard, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
const LINKS = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/connections", label: "Git-Verbindungen", icon: GitBranch },
];
export function TopNav() {
const pathname = usePathname();
return (
<header className="sticky top-0 z-40 border-b border-white/5 bg-[#05060f]/70 backdrop-blur-xl">
<div className="mx-auto flex w-full max-w-[1400px] items-center gap-6 px-4 py-3 sm:px-6 lg:px-8">
<Link href="/" className="group flex items-center gap-3">
<span className="relative grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-violet-500 via-fuchsia-500 to-cyan-400 shadow-[0_8px_28px_-10px_rgba(168,85,247,0.9)] transition-transform group-hover:scale-105">
<Sparkles className="h-4 w-4 text-white" strokeWidth={2.4} />
</span>
<span className="flex flex-col leading-none">
<span className="text-gradient text-lg font-semibold tracking-tight">
Nebula
</span>
<span className="mt-0.5 text-[10px] uppercase tracking-[0.2em] text-slate-500">
Vibe Coding
</span>
</span>
</Link>
<nav className="flex items-center gap-1">
{LINKS.map(({ href, label, icon: Icon }) => {
const active = href === "/" ? pathname === "/" : pathname.startsWith(href);
return (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors",
active
? "bg-white/8 text-white"
: "text-slate-400 hover:bg-white/5 hover:text-slate-100",
)}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline">{label}</span>
</Link>
);
})}
</nav>
</div>
</header>
);
}
+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>
);
}