/* ========================================================================== boehmitools · gemeinsame Shell Stellt App-Bar, Tool-Umschalter, Theme und kleine Helfer für alle Plugins. Einbindung in einem Plugin (siehe plugins/README.md):
... Der Mount-Pfad wird automatisch aus der URL abgeleitet, ein Plugin muss ihn nicht kennen. API-Aufrufe laufen über BT.api("/api/..."). ========================================================================== */ (function () { "use strict"; const ROOT_META = document.querySelector('meta[name="bt-root"]'); const pathMatch = location.pathname.match(/^(.*?)(\/plugins\/[^/]+)/); const BT = { /** Präfix der gesamten Suite (nur bei Reverse-Proxy-Unterpfad gesetzt). */ root: ROOT_META ? ROOT_META.content.replace(/\/$/, "") : (pathMatch ? pathMatch[1] : ""), /** Mount-Präfix des aktuellen Plugins, z. B. "/plugins/trainingsplan". */ base: pathMatch ? pathMatch[1] + pathMatch[2] : "", /** ID des aktuellen Plugins oder null (Dashboard). */ plugin: pathMatch ? pathMatch[2].split("/").pop() : null, plugins: [], }; window.BT = BT; /* ------------------------------------------------------------- Helfer */ /** Plugin-eigener Pfad → absolute URL. */ BT.url = (path) => BT.base + path; /** Suite-weiter Pfad → absolute URL. */ BT.rootUrl = (path) => BT.root + path; /** fetch mit JSON-Handling, relativ zum Plugin-Mount. */ BT.api = async function (path, options = {}) { const opts = { ...options }; if (opts.body !== undefined && !(opts.body instanceof FormData)) { opts.headers = { "Content-Type": "application/json", ...(opts.headers || {}) }; } const res = await fetch(BT.url(path), opts); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; } if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`); return data; }; BT.toast = function (message, kind = "") { let host = document.getElementById("bt-toaster"); if (!host) { host = document.createElement("div"); host.id = "bt-toaster"; document.body.appendChild(host); } const el = document.createElement("div"); el.className = "bt-toast " + kind; el.textContent = message; host.appendChild(el); requestAnimationFrame(() => el.classList.add("show")); setTimeout(() => { el.classList.remove("show"); setTimeout(() => el.remove(), 250); }, kind === "err" ? 5200 : 2600); }; BT.escape = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ( { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] )); /* -------------------------------------------------------------- Theme */ const THEME_KEY = "boehmitools.theme"; BT.setTheme = function (mode) { const resolved = mode === "auto" ? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : mode; document.documentElement.setAttribute("data-bt-theme", resolved); try { localStorage.setItem(THEME_KEY, mode); } catch (_) {} document.querySelectorAll("[data-bt-theme-toggle]").forEach((b) => { b.textContent = resolved === "dark" ? "☀︎" : "☾"; b.title = resolved === "dark" ? "Helles Design" : "Dunkles Design"; }); }; BT.currentTheme = function () { try { return localStorage.getItem(THEME_KEY) || "auto"; } catch (_) { return "auto"; } }; // so früh wie möglich anwenden (verhindert Aufblitzen) BT.setTheme(BT.currentTheme()); matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (BT.currentTheme() === "auto") BT.setTheme("auto"); }); /* ------------------------------------------------------------ App-Bar */ function onBody(fn) { if (document.body) fn(); else document.addEventListener("DOMContentLoaded", fn, { once: true }); } function buildAppbar(plugins) { const me = plugins.find((p) => p.id === BT.plugin); const title = document.body.dataset.btTitle || (me ? me.name : "Dashboard"); const icon = document.body.dataset.btIcon || (me ? me.icon : "▦"); const bar = document.createElement("header"); bar.className = "bt-appbar"; bar.innerHTML = ` B boehmitools ${BT.plugin ? ` ${BT.escape(icon)} ${BT.escape(title)}` : ""} `; const anchor = document.getElementById("bt-appbar") || document.body.firstChild; if (anchor && anchor.id === "bt-appbar") anchor.replaceWith(bar); else document.body.insertBefore(bar, document.body.firstChild); const btn = document.getElementById("bt-switch-btn"); const menu = document.getElementById("bt-switch-menu"); btn.addEventListener("click", (e) => { e.stopPropagation(); const open = menu.classList.toggle("open"); btn.setAttribute("aria-expanded", String(open)); }); document.addEventListener("click", () => menu.classList.remove("open")); document.addEventListener("keydown", (e) => { if (e.key === "Escape") menu.classList.remove("open"); }); bar.querySelector("[data-bt-theme-toggle]").addEventListener("click", () => { const now = document.documentElement.getAttribute("data-bt-theme"); BT.setTheme(now === "dark" ? "light" : "dark"); }); BT.setTheme(BT.currentTheme()); } BT.ready = fetch(BT.rootUrl("/api/plugins")) .then((r) => (r.ok ? r.json() : { plugins: [] })) .catch(() => ({ plugins: [] })) .then((d) => new Promise((resolve) => { BT.plugins = d.plugins || []; onBody(() => { if (document.body.dataset.btShell !== "off") buildAppbar(BT.plugins); document.dispatchEvent(new CustomEvent("bt:ready", { detail: BT })); resolve(BT); }); })); })();