Header/Navigation-Styling ueberarbeitet

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:34:49 +02:00
co-authored by Claude
parent 1aa902bdce
commit 4fd843318f
2 changed files with 90 additions and 49 deletions
+63 -25
View File
@@ -10,12 +10,37 @@
/* ============================================ /* ============================================
Top navigation (views/partials/header.ejs) Top navigation (views/partials/header.ejs)
============================================ ============================================
The bar keeps its deep navy in both themes: it frames the light content area The bar carries no surface of its own — it takes the page background and is
like the chrome of a tool, and it is the one surface that never changes as you held apart from the content only by a hairline (and, once scrolled, the
move through the app. Everything in it is quiet — slate on navy — except the faintest shadow). Everything in it is quiet; the single accent is the active
active item, which is the single accent: a thin, glowing sky rail. That rail is item, marked by a thin rail on the bottom edge. That rail is the whole point,
the whole point, since the previous header had no active state at all. since the header used to have no active state at all.
The dropdown panels follow the theme (white / slate) so they read as content. */
--nav-bg mirrors the page background per theme. It is what the focus rings and
the badge ring cut themselves out against — without it they would need a
hard-coded colour and would break in one of the two themes. */
:root { --nav-bg: #f9fafb; } /* gray-50, matches body */
.dark { --nav-bg: #111827; } /* gray-900, matches body */
.nav-brand:focus-visible,
.nav-item:focus-visible,
.icon-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--nav-bg), 0 0 0 4px rgba(37, 99, 235, 0.6);
}
.dark .nav-brand:focus-visible,
.dark .nav-item:focus-visible,
.dark .icon-btn:focus-visible {
box-shadow: 0 0 0 2px var(--nav-bg), 0 0 0 4px rgba(56, 189, 248, 0.7);
}
/* Hairline separation only once content actually slides underneath. */
#siteHeader.is-scrolled {
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
}
.dark #siteHeader.is-scrolled {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.nav-item { .nav-item {
position: relative; position: relative;
@@ -27,22 +52,23 @@
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
letter-spacing: -0.01em; letter-spacing: -0.01em;
color: rgb(148 163 184); /* slate-400 */ color: rgb(71 85 105); /* slate-600 */
white-space: nowrap; white-space: nowrap;
transition: color 150ms ease, background-color 150ms ease; transition: color 150ms ease, background-color 150ms ease;
} }
button.nav-item, .icon-btn { cursor: pointer; } button.nav-item, .icon-btn { cursor: pointer; }
.nav-item:hover { .nav-item:hover {
color: rgb(15 23 42); /* slate-900 */
background-color: rgba(15, 23, 42, 0.05);
}
.nav-item.is-active { color: rgb(15 23 42); }
.dark .nav-item { color: rgb(148 163 184); } /* slate-400 */
.dark .nav-item:hover {
color: #fff; color: #fff;
background-color: rgba(255, 255, 255, 0.06); background-color: rgba(255, 255, 255, 0.06);
} }
.nav-item.is-active { .dark .nav-item.is-active { color: #fff; }
color: #fff;
}
.nav-item:focus-visible {
outline: none;
box-shadow: 0 0 0 2px #0b1220, 0 0 0 4px rgba(56, 189, 248, 0.7);
}
/* The rail: sits on the header's bottom edge, lights up for the active section /* The rail: sits on the header's bottom edge, lights up for the active section
and previews faintly on hover. */ and previews faintly on hover. */
@@ -58,14 +84,18 @@ button.nav-item, .icon-btn { cursor: pointer; }
transition: transform 200ms ease, background-color 200ms ease; transition: transform 200ms ease, background-color 200ms ease;
} }
.nav-item:hover .nav-rail { .nav-item:hover .nav-rail {
background-color: rgba(148, 163, 184, 0.45); background-color: rgba(100, 116, 139, 0.35);
transform: scaleX(1); transform: scaleX(1);
} }
.nav-item.is-active .nav-rail { .nav-item.is-active .nav-rail {
background-color: rgb(56 189 248); /* sky-400 */ background-color: rgb(37 99 235); /* blue-600, the app's action colour */
box-shadow: 0 0 12px rgba(56, 189, 248, 0.8);
transform: scaleX(1); transform: scaleX(1);
} }
.dark .nav-item:hover .nav-rail { background-color: rgba(148, 163, 184, 0.4); }
.dark .nav-item.is-active .nav-rail {
background-color: rgb(56 189 248); /* sky-400 reads better on the dark page */
box-shadow: 0 0 10px rgba(56, 189, 248, 0.6);
}
/* Counters shown inline next to a nav label. /* Counters shown inline next to a nav label.
`:not(.hidden)` is deliberate: a bare `display: inline-flex` would fight `:not(.hidden)` is deliberate: a bare `display: inline-flex` would fight
@@ -97,17 +127,18 @@ button.nav-item, .icon-btn { cursor: pointer; }
width: 2.25rem; width: 2.25rem;
height: 2.25rem; height: 2.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: rgb(148 163 184); color: rgb(71 85 105); /* slate-600 */
transition: color 150ms ease, background-color 150ms ease; transition: color 150ms ease, background-color 150ms ease;
} }
.icon-btn:hover { .icon-btn:hover {
color: rgb(15 23 42);
background-color: rgba(15, 23, 42, 0.05);
}
.dark .icon-btn { color: rgb(148 163 184); }
.dark .icon-btn:hover {
color: #fff; color: #fff;
background-color: rgba(255, 255, 255, 0.08); background-color: rgba(255, 255, 255, 0.08);
} }
.icon-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 2px #0b1220, 0 0 0 4px rgba(56, 189, 248, 0.7);
}
/* Count badge floating on an icon button. Same `:not(.hidden)` reason as above. */ /* Count badge floating on an icon button. Same `:not(.hidden)` reason as above. */
.dot-badge:not(.hidden) { .dot-badge:not(.hidden) {
@@ -127,7 +158,7 @@ button.nav-item, .icon-btn { cursor: pointer; }
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
color: #fff; color: #fff;
box-shadow: 0 0 0 2px #0b1220; box-shadow: 0 0 0 2px var(--nav-bg); /* cut out against the bar in both themes */
} }
.dot-badge--red { background-color: rgb(244 63 94); } .dot-badge--red { background-color: rgb(244 63 94); }
@@ -216,11 +247,18 @@ nav .dd-panel { right: auto; left: 0; }
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 500; font-weight: 500;
color: rgb(148 163 184); color: rgb(71 85 105); /* slate-600 */
transition: background-color 150ms ease, color 150ms ease; transition: background-color 150ms ease, color 150ms ease;
} }
.m-item:hover { background-color: rgba(255, 255, 255, 0.06); color: #fff; } .m-item:hover { background-color: rgba(15, 23, 42, 0.05); color: rgb(15 23 42); }
.m-item.is-active { .m-item.is-active {
color: rgb(29 78 216); /* blue-700 */
background-color: rgb(239 246 255); /* blue-50 */
box-shadow: inset 2px 0 0 rgb(37 99 235);
}
.dark .m-item { color: rgb(148 163 184); }
.dark .m-item:hover { background-color: rgba(255, 255, 255, 0.06); color: #fff; }
.dark .m-item.is-active {
color: #fff; color: #fff;
background-color: rgba(56, 189, 248, 0.12); background-color: rgba(56, 189, 248, 0.12);
box-shadow: inset 2px 0 0 rgb(56 189 248); box-shadow: inset 2px 0 0 rgb(56 189 248);
+27 -24
View File
@@ -24,30 +24,31 @@
var angemeldet = (typeof user !== 'undefined' && user); var angemeldet = (typeof user !== 'undefined' && user);
var initiale = angemeldet ? String(user.username || '?').charAt(0).toUpperCase() : '?'; var initiale = angemeldet ? String(user.username || '?').charAt(0).toUpperCase() : '?';
%> %>
<!-- The bar carries no surface of its own: it takes the page background, so it
reads as part of the page rather than as a lid on top of it. Held apart from
the content only by a hairline and, once scrolled, the faintest shadow.
Slightly translucent + blurred so content passing underneath stays quiet. -->
<header id="siteHeader" <header id="siteHeader"
class="sticky top-0 z-50 bg-[#0b1220] text-slate-300 border-b border-white/5 transition-shadow duration-300"> class="sticky top-0 z-50 border-b border-gray-200/80 bg-gray-50/85 backdrop-blur-md transition-shadow duration-300 dark:border-white/5 dark:bg-gray-900/85">
<!-- A single cool glow anchored at the brand, instead of the old full-width <div class="container mx-auto px-4">
left-to-right gradient sweep. Gives the bar depth without shouting. -->
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(900px_circle_at_0%_-40%,rgba(56,189,248,0.16),transparent_60%)]"></div>
<div class="relative container mx-auto px-4">
<div class="flex h-16 items-center gap-3"> <div class="flex h-16 items-center gap-3">
<!-- Brand --> <!-- Brand -->
<a href="/" class="group flex shrink-0 items-center gap-2.5 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b1220]"> <a href="/" class="nav-brand group flex shrink-0 items-center gap-2.5 rounded-lg">
<span class="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-sky-400 to-blue-600 shadow-lg shadow-blue-900/40 ring-1 ring-white/10"> <span class="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-sky-400 to-blue-600 shadow-sm shadow-blue-600/20 ring-1 ring-black/5">
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg> </svg>
</span> </span>
<span class="truncate text-[15px] font-semibold tracking-tight text-white"> <span class="truncate text-[15px] font-semibold tracking-tight text-gray-900 dark:text-white">
Bewerbungs-Tracker Bewerbungs-Tracker
</span> </span>
</a> </a>
<!-- Primary navigation (desktop). Order follows the actual workflow: <!-- Primary navigation (desktop), right-aligned next to the account
finden -> bewerben -> antworten. --> cluster. Order follows the actual workflow: finden -> bewerben ->
<nav class="ml-4 hidden items-center gap-1 lg:flex" aria-label="Hauptnavigation"> antworten. -->
<nav class="ml-auto hidden items-center gap-1 lg:flex" aria-label="Hauptnavigation">
<a href="/" class="nav-item <%= istBewerbungen ? 'is-active' : '' %>"> <a href="/" class="nav-item <%= istBewerbungen ? 'is-active' : '' %>">
<span>Bewerbungen</span> <span>Bewerbungen</span>
@@ -114,8 +115,10 @@
</a> </a>
</nav> </nav>
<!-- Right cluster: notifications + account --> <!-- Right cluster: notifications + account. Below lg the nav is hidden,
<div class="ml-auto flex items-center gap-1.5"> so `ml-auto` keeps this pinned right; from lg the nav already claims
the free space and the cluster just follows it. -->
<div class="ml-auto flex items-center gap-1.5 lg:ml-3">
<!-- Notifications --> <!-- Notifications -->
<div class="relative"> <div class="relative">
@@ -143,8 +146,8 @@
<!-- Account --> <!-- Account -->
<div class="relative"> <div class="relative">
<button type="button" data-dd="ddUser" aria-expanded="false" aria-haspopup="true" <button type="button" data-dd="ddUser" aria-expanded="false" aria-haspopup="true"
class="flex items-center gap-2 rounded-lg py-1.5 pl-1.5 pr-2 text-sm text-slate-300 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b1220]"> class="nav-item !gap-2 !py-1.5 !pl-1.5 !pr-2">
<span class="grid h-7 w-7 place-items-center rounded-full bg-white/10 text-[11px] font-semibold text-white ring-1 ring-white/15"> <span class="grid h-7 w-7 place-items-center rounded-full bg-blue-600/10 text-[11px] font-semibold text-blue-700 ring-1 ring-blue-600/20 dark:bg-white/10 dark:text-white dark:ring-white/15">
<%= initiale %> <%= initiale %>
</span> </span>
<span class="hidden max-w-[10rem] truncate font-medium md:inline"><%= user.username %></span> <span class="hidden max-w-[10rem] truncate font-medium md:inline"><%= user.username %></span>
@@ -205,7 +208,7 @@
<!-- Mobile navigation: the same structure, spelled out. The old header just <!-- Mobile navigation: the same structure, spelled out. The old header just
dropped the labels and left seven unlabelled icons. --> dropped the labels and left seven unlabelled icons. -->
<div id="mobileNav" class="hidden border-t border-white/5 bg-[#0b1220] lg:hidden"> <div id="mobileNav" class="hidden border-t border-gray-200/80 bg-gray-50 lg:hidden dark:border-white/5 dark:bg-gray-900">
<nav class="container mx-auto space-y-4 px-4 py-4" aria-label="Hauptnavigation (mobil)"> <nav class="container mx-auto space-y-4 px-4 py-4" aria-label="Hauptnavigation (mobil)">
<div class="space-y-0.5"> <div class="space-y-0.5">
<a href="/" class="m-item <%= istBewerbungen ? 'is-active' : '' %>">Bewerbungen</a> <a href="/" class="m-item <%= istBewerbungen ? 'is-active' : '' %>">Bewerbungen</a>
@@ -230,15 +233,15 @@
<% if (angemeldet) { %> <% if (angemeldet) { %>
<div class="space-y-0.5"> <div class="space-y-0.5">
<p class="m-label">Konto</p> <p class="m-label">Konto</p>
<p class="px-3 pb-1 text-sm text-white"> <p class="px-3 pb-1 text-sm text-gray-900 dark:text-white">
<%= user.username %> <%= user.username %>
<span class="text-slate-500">· <%= user.is_admin ? 'Administrator' : 'Benutzer' %></span> <span class="text-gray-500 dark:text-slate-500">· <%= user.is_admin ? 'Administrator' : 'Benutzer' %></span>
</p> </p>
<a href="/einstellungen" class="m-item <%= istEinstellungen ? 'is-active' : '' %>">Einstellungen</a> <a href="/einstellungen" class="m-item <%= istEinstellungen ? 'is-active' : '' %>">Einstellungen</a>
<% if (user.is_admin) { %> <% if (user.is_admin) { %>
<a href="/admin" class="m-item <%= istAdmin ? 'is-active' : '' %>">Benutzer verwalten</a> <a href="/admin" class="m-item <%= istAdmin ? 'is-active' : '' %>">Benutzer verwalten</a>
<% } %> <% } %>
<a href="/logout" class="m-item text-rose-300 hover:bg-rose-500/10 hover:text-rose-200">Abmelden</a> <a href="/logout" class="m-item !text-rose-600 hover:!bg-rose-50 dark:!text-rose-300 dark:hover:!bg-rose-500/10">Abmelden</a>
</div> </div>
<% } %> <% } %>
</nav> </nav>
@@ -308,13 +311,13 @@
}); });
} }
// Subtle lift once the page is scrolled — the bar reads as floating // The bar has no surface of its own, so once content starts sliding
// above the content rather than welded to it. // underneath it needs the faintest separation — a hairline shadow, not
// a drop shadow.
var head = document.getElementById('siteHeader'); var head = document.getElementById('siteHeader');
function onScroll() { function onScroll() {
if (!head) return; if (!head) return;
head.classList.toggle('shadow-xl', window.scrollY > 4); head.classList.toggle('is-scrolled', window.scrollY > 4);
head.classList.toggle('shadow-black/30', window.scrollY > 4);
} }
window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('scroll', onScroll, { passive: true });
onScroll(); onScroll();