/* JuConnect · Tool-first UI interactions (v0.2) */ const $ = (sel, root=document) => root.querySelector(sel); const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); function normalizeHexColor(value) { if (!value) return ""; const trimmed = value.trim(); if (trimmed.startsWith("#")) return trimmed; const match = trimmed.match(/^rgba?\(([^)]+)\)$/i); if (!match) return trimmed; const parts = match[1].split(",").map(p => p.trim()); if (parts.length < 3) return trimmed; const toHex = (n) => { const num = Number.parseInt(n, 10); if (Number.isNaN(num)) return "00"; return Math.max(0, Math.min(255, num)).toString(16).padStart(2, "0"); }; return `#${toHex(parts[0])}${toHex(parts[1])}${toHex(parts[2])}`.toLowerCase(); } function syncBackgroundSwatch() { const swatch = document.querySelector('[data-swatch][data-name="bg"]'); if (!swatch) return; const rawBg = getComputedStyle(document.documentElement).getPropertyValue("--bg"); const hex = normalizeHexColor(rawBg); if (!hex) return; swatch.setAttribute("data-hex", hex); const label = swatch.querySelector("[data-bg-hex]"); if (label) label.textContent = hex; } function syncThemeLogos() { const isDark = document.documentElement.getAttribute("data-theme") === "dark"; $$("[data-logo-light][data-logo-dark]").forEach((img) => { const src = isDark ? img.getAttribute("data-logo-dark") : img.getAttribute("data-logo-light"); if (src) img.setAttribute("src", src); }); } /* Toasts */ const Toast = (() => { const container = () => document.querySelector(".toasts"); const show = (text, timeoutMs=2600) => { const c = container(); if (!c) return; const el = document.createElement("div"); el.className = "toast"; el.innerHTML = `
`; $(".toast__text", el).textContent = text; const close = () => { el.remove(); clearTimeout(t); }; $(".toast__close", el).addEventListener("click", close); c.appendChild(el); const t = setTimeout(close, timeoutMs); }; return { show }; })(); /* Equalize steps+image pattern column heights */ (() => { const collectColumns = (layout) => { const inner = layout.querySelector(":scope > .wp-block-group__inner-container"); if (inner) return Array.from(inner.children); return Array.from(layout.children); }; const hasTiles = (el) => !!el.querySelector(".grid.grid--4, .wp-block-group.grid.grid--4"); const hasImage = (el) => !!el.querySelector(".wp-block-image img, img"); const equalize = () => { const layouts = $$(".section .grid.grid--2, .wp-block-group.section .wp-block-group.grid.grid--2"); layouts.forEach((layout) => { const columns = collectColumns(layout).filter((el) => el.nodeType === 1); if (columns.length < 2) return; const tilesCol = columns.find(hasTiles); const imageCol = columns.find((el) => el !== tilesCol && hasImage(el)); if (!tilesCol || !imageCol) return; tilesCol.style.minHeight = ""; imageCol.style.minHeight = ""; const h = Math.max(tilesCol.offsetHeight, imageCol.offsetHeight); if (!h) return; tilesCol.style.minHeight = `${h}px`; imageCol.style.minHeight = `${h}px`; }); }; let raf = 0; const schedule = () => { if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(equalize); }; window.addEventListener("load", schedule); window.addEventListener("resize", schedule); schedule(); })(); /* Clipboard copy helper */ async function copyText(text, label="Kopiert.") { try { await navigator.clipboard.writeText(text); Toast.show(label); } catch { // Fallback const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); Toast.show(label); } } function decodeCopyText(text) { if (!text) return ""; return text .replace(/\\r\\n/g, "\n") .replace(/\\n/g, "\n") .replace(/\\t/g, "\t"); } /* Generic [data-copy] buttons */ document.addEventListener("click", (e) => { const btn = e.target.closest("[data-copy]"); if (!btn) return; const text = decodeCopyText(btn.getAttribute("data-copy") || ""); const label = btn.getAttribute("data-copy-label") || "Kopiert."; copyText(text, label); }); /* Copy for snippet blocks */ document.addEventListener("click", (e) => { const btn = e.target.closest("[data-snippet-copy]"); if (!btn) return; const snippet = btn.closest("[data-snippet]") || btn.closest(".card"); const code = snippet?.querySelector("pre code"); if (!code) return; copyText(code.innerText, btn.getAttribute("data-copy-label") || "Snippet kopiert"); }); /* Swatch copy buttons */ document.addEventListener("click", (e) => { const btn = e.target.closest("[data-swatch-copy]"); if (!btn) return; const swatch = btn.closest("[data-swatch]"); if (!swatch) return; const mode = btn.getAttribute("data-swatch-copy"); const name = swatch.getAttribute("data-name") || ""; const hex = swatch.getAttribute("data-hex") || ""; if (mode === "hex") return copyText(hex, "Hex kopiert"); if (mode === "token") return copyText(`var(--${name})`, "Token kopiert"); }); /* Theme toggle */ (() => { const btn = document.querySelector("[data-theme-toggle]"); const saved = localStorage.getItem("juconnect_theme"); if (saved) document.documentElement.setAttribute("data-theme", saved); syncBackgroundSwatch(); syncThemeLogos(); btn?.addEventListener("click", () => { const current = document.documentElement.getAttribute("data-theme"); const next = current === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); localStorage.setItem("juconnect_theme", next); syncBackgroundSwatch(); syncThemeLogos(); Toast.show(`Theme: ${next}`); }); })(); /* Active nav link on scroll */ (() => { const links = $$(".navlink"); const sections = links .map(a => document.querySelector(a.getAttribute("href"))) .filter(Boolean); const obs = new IntersectionObserver((entries) => { const visible = entries .filter(e => e.isIntersecting) .sort((a,b) => b.intersectionRatio - a.intersectionRatio)[0]; if (!visible) return; links.forEach(a => a.classList.remove("is-active")); const id = "#" + visible.target.id; const active = links.find(a => a.getAttribute("href") === id); active?.classList.add("is-active"); }, { rootMargin: "-25% 0px -65% 0px", threshold: [0.12, 0.25, 0.45] }); sections.forEach(s => obs.observe(s)); })(); /* Nav search filter */ (() => { const input = document.querySelector("[data-nav-search]"); const nav = document.querySelector("[data-nav]"); if (!input || !nav) return; const allLinks = $$("a.navlink", nav); input.addEventListener("input", () => { const q = input.value.trim().toLowerCase(); allLinks.forEach(a => { const show = !q || a.textContent.toLowerCase().includes(q) || a.getAttribute("href").toLowerCase().includes(q); a.style.display = show ? "" : "none"; }); // hide empty groups $$(".navgroup", nav).forEach(g => { const anyVisible = $$("a.navlink", g).some(a => a.style.display !== "none"); g.style.display = anyVisible ? "" : "none"; }); }); })(); /* Bildsprache prompt generator */ (() => { const topicInput = document.querySelector("[data-bild-topic]"); const copyBtn = document.querySelector("[data-bild-copy]"); if (!topicInput || !copyBtn) return; const template = `Erstelle eine ruhige, sachliche Illustration zum Thema: {THEMA}. Stil: - reduzierte, professionelle Vektorillustration - klare Formen, weiche Kanten, keine Comic-\u00dcberzeichnung - keine Verniedlichung, keine \u00fcbertriebenen Emotionen - sachlich, freundlich, respektvoll - geeignet f\u00fcr Jugend- und Familienhilfe im Kontext \u00f6ffentlicher Tr\u00e4ger Farbwelt: - Prim\u00e4rfarbe: tiefes, seri\u00f6ses Blau (#1d354f) f\u00fcr Struktur, Kleidung, Rahmen - Akzentfarbe: dezenter, warmer Salbeiton (#8FAE9A) nur f\u00fcr kleine Hervorhebungen - neutrale Off-White- und Grau-T\u00f6ne f\u00fcr Hintergr\u00fcnde - keine grellen Farben, kein hoher Kontrast, kein Schwarz Motivik: - abstrahierte Menschen oder Symbole (keine realistischen Portr\u00e4ts) - keine konkreten Alters-, Ethnie- oder Rollenklischees - Fokus auf Handlung, Beziehung oder Prozess \u2013 nicht auf Drama - positive, ruhige K\u00f6rpersprache - ausreichend Freiraum (Whitespace), damit Text erg\u00e4nzt werden kann Komposition: - klarer Bildaufbau, gut lesbar auch in klein - geeignet f\u00fcr Website-Sektionen, Infoboxen oder Erkl\u00e4rgrafiken - Hintergrund ruhig und nicht detailreich Ausschl\u00fcsse: - keine Fotos, kein Fotorealismus - keine Stock-Illustrations-Klischees - keine kindlichen Comic-Stile - keine starken Schatten, Glows oder Effekte`; const buildPrompt = () => { const topic = topicInput.value.trim() || "[THEMA EINF\u00dcGEN]"; return template.replace("{THEMA}", topic); }; copyBtn.addEventListener("click", () => { copyText(buildPrompt(), copyBtn.getAttribute("data-copy-label") || "Bildprompt kopiert"); }); })(); /* Tabs */ (() => { $$("[data-tabs]").forEach((tabs) => { const buttons = $$(".tab", tabs); const panels = $$(".tabs__panel", tabs); const activate = (btn) => { buttons.forEach(b => { const active = b === btn; b.classList.toggle("is-active", active); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); const id = btn.getAttribute("aria-controls"); panels.forEach(p => p.classList.toggle("is-active", p.id === id)); }; buttons.forEach((btn) => { btn.addEventListener("click", () => activate(btn)); btn.addEventListener("keydown", (e) => { const idx = buttons.indexOf(btn); if (e.key === "ArrowRight") { e.preventDefault(); buttons[(idx + 1) % buttons.length].focus(); } if (e.key === "ArrowLeft") { e.preventDefault(); buttons[(idx - 1 + buttons.length) % buttons.length].focus(); } if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activate(btn); } }); }); }); })(); /* Accordion */ (() => { $$("[data-accordion]").forEach((acc) => { $$(".acc__trigger", acc).forEach((btn) => { const panel = btn.nextElementSibling; if (!panel) return; btn.addEventListener("click", () => { const open = btn.getAttribute("aria-expanded") === "true"; btn.setAttribute("aria-expanded", String(!open)); panel.hidden = open; }); }); }); })(); /* Modal */ (() => { let lastFocus = null; const openModal = (modal) => { if (!modal) return; lastFocus = document.activeElement; modal.hidden = false; const focusables = $$("button,[href],input,select,textarea,[tabindex]:not([tabindex='-1'])", modal) .filter(el => !el.hasAttribute("disabled")); const first = focusables[0]; const last = focusables[focusables.length - 1]; first?.focus(); const onKey = (e) => { if (e.key === "Escape") closeModal(modal); if (e.key !== "Tab") return; if (!focusables.length) return; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }; modal.__onKey = onKey; document.addEventListener("keydown", onKey); }; const closeModal = (modal) => { if (!modal) return; modal.hidden = true; document.removeEventListener("keydown", modal.__onKey); lastFocus?.focus?.(); }; document.addEventListener("click", (e) => { const openBtn = e.target.closest("[data-modal-open]"); if (openBtn) { const sel = openBtn.getAttribute("data-modal-open"); openModal(document.querySelector(sel)); return; } const closeBtn = e.target.closest("[data-modal-close]"); if (closeBtn) { const modal = closeBtn.closest(".modal"); closeModal(modal); } }); })(); /* Demo form validation */ (() => { const form = document.querySelector("[data-demo-form]"); if (!form) return; const status = $(".form__status", form); const setStatus = (msg, type="info") => { status.textContent = msg; status.style.marginTop = "6px"; status.style.color = type === "danger" ? "var(--danger)" : "var(--muted)"; }; form.addEventListener("submit", (e) => { e.preventDefault(); const name = form.name.value.trim(); const email = form.email.value.trim(); const topic = form.topic.value.trim(); const privacy = form.privacy.checked; const errors = []; if (!name) errors.push("Name fehlt."); if (!email || !email.includes("@")) errors.push("E-Mail ungültig."); if (!topic) errors.push("Thema auswählen."); if (!privacy) errors.push("Datenschutz bestätigen."); if (errors.length) { setStatus("Bitte prüfen: " + errors.join(" "), "danger"); Toast.show("Formular unvollständig."); return; } setStatus("Demo: Formular wäre jetzt versendet.", "info"); Toast.show("Danke. Anfrage gespeichert (Demo)."); form.reset(); }); form.addEventListener("reset", () => setStatus("Zurückgesetzt.")); })();