diff --git a/assets/css/main.css b/assets/css/main.css
index 368108a..c6d454a 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -191,6 +191,27 @@ header.site-header {
font-size: .88rem;
}
+/* Interner Login — dezent, aber auf jeder Seite oben rechts auffindbar */
+.nav-login {
+ display: inline-flex;
+ align-items: center;
+ gap: .3rem;
+ padding: .5rem .9rem;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ font-weight: 700;
+ font-size: .82rem;
+ white-space: nowrap;
+ transition: color .2s ease, border-color .2s ease, background .2s ease;
+}
+.nav-login:hover,
+.nav-login:focus-visible {
+ color: var(--accent);
+ border-color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
+}
+
.nav-toggle {
display: none;
background: none;
@@ -220,7 +241,8 @@ header.site-header {
.nav-flat { display: block; padding: .9rem 0; border-bottom: 1px solid var(--border); }
.nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
.nav-group.open .nav-dropdown { display: flex; }
- .nav-cta { align-self: flex-start; margin: 1rem 0; }
+ .nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
+ .nav-login { align-self: flex-start; }
}
/* ---------- Buttons ---------- */
@@ -949,6 +971,9 @@ footer.site-footer {
}
}
+/* Kurzes, dezentes Abblenden während ein Seitenwechsel geholt wird */
+body.seite-laedt main { opacity: .55; transition: opacity .15s ease; }
+
/* ==========================================================================
MUSIK-KNOPF — kleiner schwebender Knopf unten rechts, auf jeder Seite
diff --git a/assets/js/main.js b/assets/js/main.js
index ba8d110..b7966d5 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -52,7 +52,8 @@
{ label: "Links", href: "links.html" },
];
- const currentFile = (location.pathname.split("/").pop() || "index.html");
+ /* Aktuelle Datei — wird beim nahtlosen Seitenwechsel mit aktualisiert. */
+ let currentFile = (location.pathname.split("/").pop() || "index.html");
function isActive(href) {
return href === currentFile;
@@ -93,6 +94,11 @@
`;
@@ -114,13 +120,18 @@
});
});
- document.addEventListener("click", (e) => {
- if (!el.contains(e.target)) {
- el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
- }
- });
}
+ /* Klick außerhalb schließt offene Menüs. Bewusst NUR EINMAL angemeldet —
+ renderHeader() läuft bei jedem Seitenwechsel erneut, und ein Zuhörer
+ pro Wechsel würde sich sonst endlos aufsummieren. */
+ document.addEventListener("click", (e) => {
+ const kopf = document.getElementById("site-header");
+ if (kopf && !kopf.contains(e.target)) {
+ kopf.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
+ }
+ });
+
function renderFooter() {
const el = document.getElementById("site-footer");
if (!el) return;
@@ -260,10 +271,159 @@
});
}
+ /* ===================================================================
+ Nahtloser Seitenwechsel — damit die Musik nicht abreißt
+
+ Normalerweise lädt der Browser bei jedem Klick die ganze Seite neu,
+ wodurch der Spotify-Player jedes Mal von vorn startet. Stattdessen
+ holen wir die Zielseite im Hintergrund und tauschen nur den
+ Inhaltsbereich aus. Kopfzeile, Fußzeile und der Musik-Knopf
+ bleiben stehen — die Musik läuft einfach weiter.
+
+ Wichtig für die Sicherheit: Die internen Seiten (Postfach, Zugänge)
+ sind bewusst AUSGENOMMEN und laden weiterhin ganz normal. An Login,
+ Sitzung und Rechten ändert sich dadurch nichts.
+
+ Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
+ zur Zielseite navigiert. Schlimmstenfalls ist es also wie vorher.
+ =================================================================== */
+ const KEIN_NAHTLOSER_WECHSEL = ["postfach.html", "zugaenge.html"];
+
+ /* Seiteneigene Skripte melden sich fast alle über "DOMContentLoaded" an.
+ Nach dem ersten Laden ist dieses Ereignis längst vorbei — ein später
+ nachgeladenes Skript würde also nie starten. Deshalb: Anmeldungen, die
+ NACH dem Laden eintreffen, sofort ausführen. Während des normalen
+ Erstaufbaus (readyState === "loading") bleibt alles wie gehabt. */
+ const anmeldenOriginal = document.addEventListener.bind(document);
+ document.addEventListener = function (typ, fn, optionen) {
+ if (typ === "DOMContentLoaded" && document.readyState !== "loading" && typeof fn === "function") {
+ setTimeout(() => fn(new Event("DOMContentLoaded")), 0);
+ return;
+ }
+ return anmeldenOriginal(typ, fn, optionen);
+ };
+
+ /** Skripte der neuen Seite der Reihe nach ausführen. */
+ async function skripteUebernehmen(neuesDokument) {
+ const schonGeladen = new Set(
+ [...document.querySelectorAll("script[src]")].map((s) => s.getAttribute("src"))
+ );
+
+ for (const alt of [...neuesDokument.querySelectorAll("script")]) {
+ const src = alt.getAttribute("src");
+
+ if (src) {
+ // Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei):
+ // nicht erneut ausführen — sonst gäbe es doppelte Deklarationen.
+ if (schonGeladen.has(src)) continue;
+ schonGeladen.add(src);
+ await new Promise((fertig) => {
+ const neu = document.createElement("script");
+ neu.src = src;
+ neu.async = false;
+ neu.onload = neu.onerror = fertig; // auch bei Fehler weitermachen
+ document.body.appendChild(neu);
+ });
+ } else {
+ // Seiteneigenes Skript (z.B. Team-Vorschau der Startseite).
+ // Läuft erst, wenn die Datendateien davor fertig geladen sind.
+ const neu = document.createElement("script");
+ neu.textContent = alt.textContent;
+ document.body.appendChild(neu);
+ }
+ }
+ }
+
+ let ladeVorgang = 0;
+
+ async function seiteLaden(url, mitVerlauf) {
+ const lauf = ++ladeVorgang;
+ document.body.classList.add("seite-laedt");
+
+ let dok;
+ try {
+ const antwort = await fetch(url, { credentials: "same-origin" });
+ if (!antwort.ok) throw new Error("HTTP " + antwort.status);
+ dok = new DOMParser().parseFromString(await antwort.text(), "text/html");
+ } catch {
+ location.href = url; // Rückfall: ganz normal navigieren
+ return;
+ }
+ if (lauf !== ladeVorgang) return; // zwischenzeitlich woanders hingeklickt
+
+ const neuerInhalt = dok.querySelector("main");
+ const alterInhalt = document.querySelector("main");
+ if (!neuerInhalt || !alterInhalt) {
+ location.href = url;
+ return;
+ }
+
+ alterInhalt.replaceWith(neuerInhalt);
+
+ // Kopf der Seite mitziehen: Titel, Farbwelt, Beschreibung
+ document.title = dok.title;
+ const themaNeu = dok.documentElement.getAttribute("data-theme");
+ if (themaNeu) document.documentElement.setAttribute("data-theme", themaNeu);
+ const beschreibungNeu = dok.querySelector('meta[name="description"]');
+ const beschreibungAlt = document.querySelector('meta[name="description"]');
+ if (beschreibungNeu && beschreibungAlt) {
+ beschreibungAlt.setAttribute("content", beschreibungNeu.getAttribute("content"));
+ }
+
+ if (mitVerlauf) history.pushState({ nahtlos: true }, "", url);
+ currentFile = (new URL(url, location.href).pathname.split("/").pop() || "index.html");
+
+ renderHeader(); // markiert den richtigen Menüpunkt als aktiv
+ window.scrollTo(0, 0);
+ document.body.classList.remove("seite-laedt");
+
+ await skripteUebernehmen(dok);
+ }
+
+ function nahtloseNavigationAktivieren() {
+ // Ohne History-API oder auf file:// lieber gar nicht erst versuchen
+ if (!window.history || !window.history.pushState || location.protocol === "file:") return;
+ if (KEIN_NAHTLOSER_WECHSEL.includes(currentFile)) return;
+
+ document.addEventListener("click", (e) => {
+ if (e.defaultPrevented || e.button !== 0) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // neuer Tab o.ä.
+
+ const link = e.target.closest("a");
+ if (!link) return;
+
+ const href = link.getAttribute("href");
+ if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
+ if (link.target && link.target !== "_self") return;
+ if (link.hasAttribute("download")) return;
+
+ const ziel = new URL(href, location.href);
+ if (ziel.origin !== location.origin) return; // fremde Seite
+ if (!ziel.pathname.endsWith(".html")) return; // z.B. Bilddatei
+ const datei = ziel.pathname.split("/").pop();
+ if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) return; // interner Bereich
+ if (ziel.pathname === location.pathname && ziel.search === location.search) return;
+
+ e.preventDefault();
+ seiteLaden(ziel.href, true);
+ });
+
+ window.addEventListener("popstate", () => {
+ // Zurück-/Vorwärts-Knopf im Browser
+ const datei = location.pathname.split("/").pop() || "index.html";
+ if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) {
+ location.reload();
+ return;
+ }
+ seiteLaden(location.href, false);
+ });
+ }
+
document.addEventListener("DOMContentLoaded", () => {
renderHeader();
renderFooter();
renderFog();
renderMusicWidget();
+ nahtloseNavigationAktivieren();
});
})();