Top-Navigation: neu strukturiert, mit Aktiv-Zustand und Dropdowns

Die Leiste war eine flache Reihe aus sieben gleichrangigen Pillen. Zwei
Probleme: sie zeigte nie an, wo man gerade ist (es gab schlicht keinen
Aktiv-Zustand), und auf dem Handy fielen nur die Beschriftungen weg - uebrig
blieben sieben unbeschriftete Icons.

Struktur folgt jetzt dem tatsaechlichen Ablauf (finden -> bewerben ->
antworten) statt der Reihenfolge, in der die Seiten entstanden sind:

  Bewerbungen | Jobangebote | Stellensuche v | Postfach | Vorlagen

"Stellensuche" buendelt als Dropdown, was Angebote erzeugt und filtert:
Suchprofil & Laeufe, Uebernommene Angebote, Blacklist - jeweils mit einer Zeile,
die erklaert, wozu der Punkt gut ist. Einstellungen, Benutzerverwaltung und
Abmelden wandern ins Konto-Menue rechts, wo Systemkram hingehoert. Die
taeglichen Wege (Bewerbungen, Jobangebote, Postfach) bleiben ein Klick weit weg.

Gestaltung: Die Leiste behaelt in beiden Themes ihr dunkles Navy - sie ist der
Rahmen des Werkzeugs und die einzige Flaeche, die sich beim Navigieren nie
aendert. Alles darin ist bewusst still (Slate auf Navy); der einzige Akzent ist
der aktive Punkt, markiert durch eine schmale leuchtende Schiene an der
Unterkante. Beim Hover deutet sich dieselbe Schiene gedaempft an.

Zaehler (ungelesene Post, offene Angebote) haengen jetzt an Klassen statt IDs,
weil sie in Leiste UND Mobilmenue erscheinen. Ein Controller bedient alle
Dropdowns: Klick oeffnet, andere schliessen, Escape und Klick daneben schliessen,
aria-expanded bleibt ehrlich. Fokus ist sichtbar, prefers-reduced-motion wird
respektiert, das Mobilmenue zeigt alle Punkte ausgeschrieben und gruppiert.

Der aktive Pfad kommt aus res.locals.pfad (Auth-Middleware). Die Navigation liegt
weiterhin in genau einem Partial - sie ist damit auf allen Seiten identisch;
nur die Login-Seite hat bewusst keine.

Geprueft mit echtem Browser (Screenshots): hell/dunkel, Dropdowns, Mobilmenue,
und der Aktiv-Zustand je Seite - Unterseiten wie /blacklist oder /jobsuche heben
korrekt "Stellensuche" hervor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 23:55:58 +02:00
co-authored by Claude Opus 4.8
parent 9418750061
commit 1aa902bdce
4 changed files with 559 additions and 138 deletions
+227
View File
@@ -7,6 +7,233 @@
box-sizing: border-box;
}
/* ============================================
Top navigation (views/partials/header.ejs)
============================================
The bar keeps its deep navy in both themes: it frames the light content area
like the chrome of a tool, and it is the one surface that never changes as you
move through the app. Everything in it is quiet — slate on navy — except the
active item, which is the single accent: a thin, glowing sky rail. That rail is
the whole point, since the previous header had no active state at all.
The dropdown panels follow the theme (white / slate) so they read as content. */
.nav-item {
position: relative;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: -0.01em;
color: rgb(148 163 184); /* slate-400 */
white-space: nowrap;
transition: color 150ms ease, background-color 150ms ease;
}
button.nav-item, .icon-btn { cursor: pointer; }
.nav-item:hover {
color: #fff;
background-color: rgba(255, 255, 255, 0.06);
}
.nav-item.is-active {
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
and previews faintly on hover. */
.nav-rail {
position: absolute;
left: 0.75rem;
right: 0.75rem;
bottom: -1px;
height: 2px;
border-radius: 9999px;
background-color: transparent;
transform: scaleX(0);
transition: transform 200ms ease, background-color 200ms ease;
}
.nav-item:hover .nav-rail {
background-color: rgba(148, 163, 184, 0.45);
transform: scaleX(1);
}
.nav-item.is-active .nav-rail {
background-color: rgb(56 189 248); /* sky-400 */
box-shadow: 0 0 12px rgba(56, 189, 248, 0.8);
transform: scaleX(1);
}
/* Counters shown inline next to a nav label.
`:not(.hidden)` is deliberate: a bare `display: inline-flex` would fight
Tailwind's `.hidden` (same specificity, load order decides) and the badge would
show even at count 0. This way "hidden" always wins. */
.nav-badge:not(.hidden) {
display: inline-flex;
}
.nav-badge {
align-items: center;
justify-content: center;
min-width: 1.125rem;
height: 1.125rem;
padding: 0 0.3rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 700;
line-height: 1;
color: #fff;
}
.nav-badge--red { background-color: rgb(244 63 94); } /* rose-500 */
.nav-badge--green { background-color: rgb(16 185 129); } /* emerald-500 */
/* Icon-only controls in the bar (bell, burger). */
.icon-btn {
position: relative;
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
color: rgb(148 163 184);
transition: color 150ms ease, background-color 150ms ease;
}
.icon-btn:hover {
color: #fff;
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. */
.dot-badge:not(.hidden) {
display: inline-flex;
}
.dot-badge {
position: absolute;
top: -0.125rem;
right: -0.125rem;
align-items: center;
justify-content: center;
min-width: 1.125rem;
height: 1.125rem;
padding: 0 0.25rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 700;
line-height: 1;
color: #fff;
box-shadow: 0 0 0 2px #0b1220;
}
.dot-badge--red { background-color: rgb(244 63 94); }
/* Dropdown panels. */
.dd-panel {
position: absolute;
right: 0;
top: calc(100% + 0.5rem);
z-index: 50;
padding: 0.375rem;
border-radius: 0.75rem;
background-color: #fff;
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(15, 23, 42, 0.08);
animation: dd-in 140ms ease-out;
}
.dark .dd-panel {
background-color: rgb(30 41 59); /* slate-800 */
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.6);
}
/* Menus that hang off a left-aligned nav item open to the left edge. */
nav .dd-panel { right: auto; left: 0; }
@keyframes dd-in {
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.dd-panel { animation: none; }
.nav-rail { transition: none; }
}
.dd-item {
display: flex;
align-items: flex-start;
gap: 0.625rem;
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
color: rgb(51 65 85); /* slate-700 */
transition: background-color 150ms ease, color 150ms ease;
}
.dd-item:hover { background-color: rgb(241 245 249); } /* slate-100 */
.dark .dd-item { color: rgb(203 213 225); }
.dark .dd-item:hover { background-color: rgba(255, 255, 255, 0.06); }
.dd-item:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.8);
}
.dd-item.is-active { background-color: rgb(239 246 255); color: rgb(29 78 216); } /* blue-50/700 */
.dark .dd-item.is-active { background-color: rgba(56, 189, 248, 0.12); color: rgb(125 211 252); }
.dd-item--danger { color: rgb(190 18 60); } /* rose-700 */
.dd-item--danger:hover { background-color: rgb(255 241 242); }
.dark .dd-item--danger { color: rgb(253 164 175); }
.dark .dd-item--danger:hover { background-color: rgba(244, 63, 94, 0.12); }
.dd-icon {
width: 1.125rem;
height: 1.125rem;
flex-shrink: 0;
margin-top: 0.075rem;
opacity: 0.7;
}
.dd-title {
display: block;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.35;
}
.dd-desc {
display: block;
margin-top: 0.075rem;
font-size: 0.75rem;
line-height: 1.35;
color: rgb(100 116 139); /* slate-500 */
}
.dark .dd-desc { color: rgb(148 163 184); }
.dd-item.is-active .dd-desc { color: inherit; opacity: 0.75; }
/* Mobile menu. */
.m-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.9375rem;
font-weight: 500;
color: rgb(148 163 184);
transition: background-color 150ms ease, color 150ms ease;
}
.m-item:hover { background-color: rgba(255, 255, 255, 0.06); color: #fff; }
.m-item.is-active {
color: #fff;
background-color: rgba(56, 189, 248, 0.12);
box-shadow: inset 2px 0 0 rgb(56 189 248);
}
.m-label {
padding: 0 0.75rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgb(100 116 139);
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;