From 6fcbc041108b52ad7941cbed65eae09bbc0e4be0 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 25 Jul 2026 19:34:15 +0200 Subject: [PATCH] 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) --- src/app/error.tsx | 52 +++++ src/app/globals.css | 365 ++++++++++++++++++++++++++++++++++++ src/app/icon.svg | 15 ++ src/app/layout.tsx | 51 +++++ src/app/not-found.tsx | 19 ++ src/components/TopNav.tsx | 56 ++++++ src/components/ui/Modal.tsx | 84 +++++++++ 7 files changed, 642 insertions(+) create mode 100644 src/app/error.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/icon.svg create mode 100644 src/app/layout.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/TopNav.tsx create mode 100644 src/components/ui/Modal.tsx diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..3165b34 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useEffect } from "react"; +import { AlertTriangle, RotateCcw } from "lucide-react"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + const isDbError = + /database|prisma|ECONNREFUSED|P1001|does not exist/i.test(error.message ?? ""); + + return ( +
+ + + +

Da ist etwas schiefgelaufen

+

{error.message}

+ + {isDbError ? ( +
+

Sieht nach einem Datenbankproblem aus.

+
    +
  1. + Laeuft PostgreSQL? docker compose up -d db +
  2. +
  3. + Stimmt DATABASE_URL in der{" "} + .env? +
  4. +
  5. + Migrationen eingespielt? npm run db:migrate +
  6. +
+
+ ) : null} + + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..c71ec96 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,365 @@ +@import "tailwindcss"; + +/* ───────────────────────────────────────────────────────────── + Nebula – Designsystem + Dark Glassmorphism mit Neon-Akzenten. + ───────────────────────────────────────────────────────────── */ + +@theme { + --color-void: #05060f; + --color-void-soft: #0a0c1a; + --color-haze: #12142b; + + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, "Noto Sans", sans-serif; + --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, + Consolas, "DejaVu Sans Mono", monospace; + + --radius-card: 1.25rem; +} + +:root { + color-scheme: dark; + --glass-bg: rgba(255, 255, 255, 0.035); + --glass-border: rgba(255, 255, 255, 0.09); + --glass-highlight: rgba(255, 255, 255, 0.16); +} + +* { + border-color: var(--glass-border); +} + +html, +body { + min-height: 100%; +} + +body { + background-color: var(--color-void); + color: #e6e8f5; + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} + +/* ── Hintergrund: langsam driftende Nebel ───────────────────── */ + +.aurora { + position: fixed; + inset: 0; + z-index: -2; + overflow: hidden; + pointer-events: none; +} + +.aurora::after { + /* feines Rauschen, damit die Verlaeufe nicht bandieren */ + content: ""; + position: absolute; + inset: 0; + opacity: 0.35; + background-image: radial-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px); + background-size: 3px 3px; + mix-blend-mode: overlay; +} + +.aurora-blob { + position: absolute; + border-radius: 9999px; + filter: blur(90px); + opacity: 0.5; + will-change: transform; +} + +.aurora-blob--one { + width: 46rem; + height: 46rem; + top: -18rem; + left: -12rem; + background: radial-gradient(circle at 30% 30%, #7c3aed, transparent 65%); + animation: drift-a 26s ease-in-out infinite alternate; +} + +.aurora-blob--two { + width: 40rem; + height: 40rem; + top: -10rem; + right: -14rem; + background: radial-gradient(circle at 60% 40%, #0ea5e9, transparent 65%); + animation: drift-b 32s ease-in-out infinite alternate; +} + +.aurora-blob--three { + width: 38rem; + height: 38rem; + bottom: -20rem; + left: 28%; + background: radial-gradient(circle at 50% 50%, #db2777, transparent 68%); + animation: drift-c 38s ease-in-out infinite alternate; + opacity: 0.35; +} + +.aurora-grid { + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.028) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, #000 40%, transparent 100%); +} + +@keyframes drift-a { + from { transform: translate3d(0, 0, 0) scale(1); } + to { transform: translate3d(6rem, 4rem, 0) scale(1.12); } +} +@keyframes drift-b { + from { transform: translate3d(0, 0, 0) scale(1.05); } + to { transform: translate3d(-7rem, 5rem, 0) scale(0.95); } +} +@keyframes drift-c { + from { transform: translate3d(0, 0, 0) scale(0.95); } + to { transform: translate3d(4rem, -6rem, 0) scale(1.1); } +} + +/* ── Glasflaechen ───────────────────────────────────────────── */ + +.glass { + position: relative; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + backdrop-filter: blur(18px) saturate(140%); + -webkit-backdrop-filter: blur(18px) saturate(140%); + border-radius: var(--radius-card); + box-shadow: + 0 1px 0 0 var(--glass-highlight) inset, + 0 24px 60px -30px rgba(0, 0, 0, 0.9); +} + +.glass-soft { + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.07); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-radius: 0.9rem; +} + +/* Karte mit Akzent-Glow. --accent-from/--accent-to setzt die Komponente. */ +.card-glow { + transition: + transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 0.35s ease, + border-color 0.35s ease; +} + +.card-glow::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, var(--accent-from, #8b5cf6), var(--accent-to, #d946ef)); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; +} + +.card-glow:hover { + transform: translateY(-4px); + box-shadow: + 0 1px 0 0 var(--glass-highlight) inset, + 0 30px 70px -30px rgba(0, 0, 0, 0.95), + 0 0 46px -12px var(--accent-from, #8b5cf6); +} + +.card-glow:hover::before { + opacity: 0.85; +} + +/* ── Text & Akzente ─────────────────────────────────────────── */ + +.text-gradient { + background: linear-gradient(120deg, #c4b5fd 0%, #67e8f9 45%, #f0abfc 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.accent-bar { + background: linear-gradient(90deg, var(--accent-from, #8b5cf6), var(--accent-to, #d946ef)); +} + +.accent-dot { + box-shadow: 0 0 12px 1px currentColor; +} + +/* ── Buttons ────────────────────────────────────────────────── */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 0.75rem; + font-size: 0.875rem; + font-weight: 500; + padding: 0.55rem 1rem; + transition: all 0.2s ease; + cursor: pointer; + white-space: nowrap; + border: 1px solid transparent; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: linear-gradient(120deg, #7c3aed, #c026d3); + color: #fff; + box-shadow: 0 10px 30px -12px rgba(168, 85, 247, 0.9); +} + +.btn-primary:hover:not(:disabled) { + filter: brightness(1.12); + box-shadow: 0 14px 36px -12px rgba(168, 85, 247, 1); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.1); + color: #cbd5f5; +} + +.btn-ghost:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.09); + color: #fff; +} + +.btn-danger { + background: rgba(244, 63, 94, 0.12); + border-color: rgba(244, 63, 94, 0.35); + color: #fda4af; +} + +.btn-danger:hover:not(:disabled) { + background: rgba(244, 63, 94, 0.22); + color: #fff; +} + +/* ── Formularfelder ─────────────────────────────────────────── */ + +.field { + width: 100%; + border-radius: 0.75rem; + background: rgba(8, 10, 24, 0.6); + border: 1px solid rgba(255, 255, 255, 0.09); + padding: 0.6rem 0.85rem; + font-size: 0.875rem; + color: #e6e8f5; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.field::placeholder { + color: rgba(148, 163, 184, 0.6); +} + +.field:focus { + outline: none; + border-color: rgba(167, 139, 250, 0.6); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.18); +} + +.field-label { + display: block; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(148, 163, 184, 0.85); + margin-bottom: 0.4rem; +} + +select.field option { + background: #0a0c1a; + color: #e6e8f5; +} + +/* ── Sonstiges ──────────────────────────────────────────────── */ + +.shimmer { + background: linear-gradient( + 100deg, + rgba(255, 255, 255, 0) 20%, + rgba(255, 255, 255, 0.12) 50%, + rgba(255, 255, 255, 0) 80% + ); + background-size: 200% 100%; + animation: shimmer 2.2s linear infinite; +} + +@keyframes shimmer { + from { background-position: 200% 0; } + to { background-position: -200% 0; } +} + +.fade-up { + animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes fade-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +.pulse-ring { + animation: pulse-ring 2.4s ease-out infinite; +} + +@keyframes pulse-ring { + 0% { box-shadow: 0 0 0 0 currentColor; opacity: 0.7; } + 70% { box-shadow: 0 0 0 8px transparent; opacity: 0; } + 100% { box-shadow: 0 0 0 0 transparent; opacity: 0; } +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.22); + border-radius: 9999px; + border: 2px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(167, 139, 250, 0.5); + background-clip: content-box; +} + +::selection { + background: rgba(139, 92, 246, 0.35); + color: #fff; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } +} diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..b9e3353 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..cfe5be9 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,51 @@ +import type { Metadata, Viewport } from "next"; +import { TopNav } from "@/components/TopNav"; +import "./globals.css"; + +export const metadata: Metadata = { + title: { + default: "Nebula – Vibe Coding Dashboard", + template: "%s · Nebula", + }, + description: + "Uebersicht ueber alle Vibe-Coding Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.", + applicationName: "Nebula", +}; + +export const viewport: Viewport = { + themeColor: "#05060f", + width: "device-width", + initialScale: 1, +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + +