Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. Kontakte (Cards)

NEWS

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    13
    1
    1.0k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    2.1k

  • Verwendung von KI bitte immer deutlich kennzeichnen
    HomoranH
    Homoran
    11
    1
    972

Kontakte (Cards)

Geplant Angeheftet Gesperrt Verschoben JavaScript
javascriptmultimediatemplate
22 Beiträge 4 Kommentatoren 378 Aufrufe 5 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Ro75R Ro75

    In unserem Dashboard stehen alle wichtigen Kontakte als moderne, dynamische Karten bereit – so hat jedes Familienmitglied jederzeit Zugriff auf die aktuellsten Informationen, ohne suchen zu müssen.
    Die Daten liegen zentral in einer JSON, aus der automatisch schicke SVG‑Karten erzeugt werden, die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

    Sobald ein Eintrag geändert oder ein neuer Kontakt hinzugefügt wird, aktualisiert das System die Karten vollautomatisch.
    Keine Pflege an mehreren Stellen, keine veralteten Infos – einmal eintragen, überall aktuell.

    Das Script erzeugt beim ersten Start automatisch einen kompletten Beispieldatensatz mit drei fertigen Karten – inklusive der passenden SVG‑Grafiken.
    So sieht man sofort, wie das System funktioniert und wie die Karten später in der Visualisierung aussehen.

    card.png

    HINWEIS: Ein Teil des Codes wurde dabei mit Unterstützung einer KI (Microsoft Copilot) entwickelt, um die Erstellung noch effizienter und moderner zu gestalten.

    VORAUSSETZUNGEN:

    • JS-Adapter: 9.0.18
    • Node-JS: 22.22.3

    Hier der Code:

    //********** START KONFIGURATION **********
    const root     = "0_userdata.0.Kontakte."
    const JSON_DP  = root + "json";
    const SVG_ROOT = root + "svg.";
    
    const FONT = {
        family: "InterVariable",   // globale Schriftfamilie
    
        fields: {
            name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
            address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        weight: {
            normal: 400,  // normales Schriftgewicht
            bold:   600   // fettes Schriftgewicht
        }
    };
    
    // zentrale SVG-Größen
    const CARD_LARGE_W = 350;
    const CARD_LARGE_H = 360;
    
    const CARD_SMALL_W = 350;
    const CARD_SMALL_H = 130;
    
    const AVATAR = {
        aura: {
            inner: {color: "#6E463E", opacity: 0.22}, // stärkster innerer Aura-Kern
            mid:   {color: "#6E463E", opacity: 0.08}, // mittlere Aura
            outer: {color: "#6E463E", opacity: 0.00}  // äußerer Rand, auslaufend
        },
    
        glass: {
            core:  {color: "#ffffff", opacity: 1.00}, // hellster Kern
            glow1: {color: "#6E463E", opacity: 0.92}, // starker Glow
            glow2: {color: "#6E463E", opacity: 0.48}, // mittlerer Glow
            glow3: {color: "#6E463E", opacity: 0.12}, // schwacher Glow
            fade:  {color: "#6E463E", opacity: 0.00}  // auslaufender Rand
        },
    
        glowFactorSmall: 1.45,           // Glow‑Dunkelheits‑Faktor NUR für kleine Karte
        auraFactorSmall: 0.45,           // Aura der kleinen Karte abdunkeln
        ring: "rgba(255,255,255,0.30)",  // äußerer Ring
        background: "#0f2647"            // Avatar-Hintergrund
    };
    
    //Beispieldaten json - als Base64 - für weniger Platz
    const SAMPLE_DATA_BASE64 =
    `ewogICJlaW50cmFlZ2UiOiBbCiAgICB7CiAgICAgICJpZCI6IDEsCiAgICAgICJrYXJ0ZSI6ICJsYXJnZSIsCiAgICAgICJ0eXAiOiAiaGF1c2FyenQiLAogICAgICAi
    bmFtZSI6ICJIYXVzYXJ6dCDigJMgRHIuIE1heCBNdXN0ZXJtYW5uIiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvYXJ6dC8zMDAvMzAw
    IiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFzc2UiOiAiTXVzdGVyc3RyYcOfZSAxMiIsCiAgICAgICAgInBseiI6ICIwMTIzNCIsCiAgICAgICAgIm9y
    dCI6ICJNdXN0ZXJzdGFkdCIKICAgICAgfSwKICAgICAgInplaXRlbiI6IFsKICAgICAgICAiTW9udGFnIDA4LjAwIOKAkyAxMi4wMCIsCiAgICAgICAgIkRpZW5zdGFn
    IDA4LjAwIOKAkyAxMi4wMCwgMTQuMDAg4oCTIDE4LjAwIiwKICAgICAgICAiTWl0d29jaCAwOC4wMCDigJMgMTIuMDAiLAogICAgICAgICJEb25uZXJzdGFnIDA4LjAw
    IOKAkyAxMi4wMCIsCiAgICAgICAgIkZyZWl0YWcgMDguMDAg4oCTIDEyLjAwIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAidGVsZWZvbiI6ICIo
    MDEyMzQpIDEyMzQ1NiIsCiAgICAgICAgImZheCI6ICIoMDEyMzQpIDY1NDMyMSIsCiAgICAgICAgImVtYWlsIjogInByYXhpc0BtdXN0ZXJtYW5uLmRlIiwKICAgICAg
    ICAid2ViIjogImh0dHBzOi8vd3d3Lm11c3Rlcm1hbm4taGF1c2FyenQuZGUvIgogICAgICB9LAogICAgICAiaGlud2VpcyI6ICIiCiAgICB9LAogICAgewogICAgICAi
    aWQiOiAyLAogICAgICAia2FydGUiOiAibGFyZ2UiLAogICAgICAidHlwIjogInphaG5hcnp0IiwKICAgICAgIm5hbWUiOiAiWmFobmFyenQg4oCTIERyLiBKdWxpYSBC
    ZWlzcGllbCIsCiAgICAgICJiaWxkIjogImh0dHBzOi8vcGljc3VtLnBob3Rvcy9zZWVkL3phaG5hcnp0LzMwMC8zMDAiLAogICAgICAiYWRyZXNzZSI6IHsKICAgICAg
    ICAic3RyYXNzZSI6ICJCZWlzcGllbHdlZyA1IiwKICAgICAgICAicGx6IjogIjU2Nzg5IiwKICAgICAgICAib3J0IjogIkJlaXNwaWVsc3RhZHQiCiAgICAgIH0sCiAg
    ICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyAwOS4wMCDigJMgMTMuMDAiLAogICAgICAgICJEaWVuc3RhZyAwOS4wMCDigJMgMTMuMDAsIDE0LjAwIOKAkyAx
    OC4wMCIsCiAgICAgICAgIk1pdHR3b2NoIDA5LjAwIOKAkyAxMy4wMCIsCiAgICAgICAgIkRvbm5lcnN0YWcgMDkuMDAg4oCTIDEzLjAwLCAxNC4wMCDigJMgMTguMDAi
    LAogICAgICAgICJGcmVpdGFnIDA5LjAwIOKAkyAxMi4wMCIKICAgICAgXSwKICAgICAgImtvbnRha3QiOiB7CiAgICAgICAgInRlbGVmb24iOiAiKDA1Njc4KSA5ODc2
    NTQiLAogICAgICAgICJmYXgiOiAiIiwKICAgICAgICAiZW1haWwiOiAiaW5mb0B6YWhuYXJ6dC1iZWlzcGllbC5kZSIsCiAgICAgICAgIndlYiI6ICJodHRwczovL3d3
    dy56YWhuYXJ6dC1iZWlzcGllbC5kZS8iCiAgICAgIH0sCiAgICAgICJoaW53ZWlzIjogIlRlcm1pbmUgbmFjaCBWZXJlaW5iYXJ1bmciCiAgICB9LAogICAgewogICAg
    ICAiaWQiOiAzLAogICAgICAia2FydGUiOiAic21hbGwiLAogICAgICAidHlwIjogImZyaXNldXIiLAogICAgICAibmFtZSI6ICJGcmlzZXVyIFNhbG9uIEJlaXNwaWVs
    IiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvZnJpc2V1ci8zMDAvMzAwIiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFz
    c2UiOiAiIiwKICAgICAgICAicGx6IjogIiIsCiAgICAgICAgIm9ydCI6ICIiCiAgICAgIH0sCiAgICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyDigJMgRnJl
    aXRhZyAwOC4wMCDigJMgMTguMDAiLAogICAgICAgICJTYW1zdGFnIG5hY2ggVmVyZWluYmFydW5nIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAi
    dGVsZWZvbiI6ICIoMDEyMzQpIDExMjIzMyIsCiAgICAgICAgImZheCI6ICIiLAogICAgICAgICJlbWFpbCI6ICIiLAogICAgICAgICJ3ZWIiOiAiIgogICAgICB9LAog
    ICAgICAiaGlud2VpcyI6ICIiCiAgICB9CiAgXQp9`;
    
    /*
        JSON‑Datensatz – Anforderungen (kurz & eindeutig)
    
        • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
        • Telefonnummern dürfen mehrere Werte enthalten, getrennt durch Komma oder Semikolon.
        • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
          und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).
        • Das Bildfeld („bild“) kann eine WWW‑URL oder eine ioBroker‑interne URL sein,
          z. B. http://192.168.10.99:8082/vis.0/kontakte/xyz.png
          und es werden PNG, JPG und SVG unterstützt.
    */
    
    
    //********** ENDE KONFIGURATION **********
    
    const ICON_LOCATION = `<path d="M21 10c0 6-9 13-9 13S3 16 3 10a9 9 0 1 1 18 0z"/><circle cx="12" cy="10" r="3"/>`;
    const ICON_PHONE    = `<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.12.89.32 1.76.59 2.59a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.49-1.16a2 2 0 0 1 2.11-.45c.83.27 1.7.47 2.59.59A2 2 0 0 1 22 16.92z"/>`;
    const ICON_LIST     = `<path d="M6 18h12"/><path d="M6 14h12"/><path d="M6 10h12"/><rect x="4" y="4" width="16" height="16" rx="2"/>`;
    const ICON_MAIL     = `<path d="M4 4h16v16H4z"/><path d="M22 6l-10 7L2 6"/>`;
    const ICON_GLOBE    = `<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15 15 0 0 1 0 20"/><path d="M12 2a15 15 0 0 0 0 20"/>`;
    const ICON_CLOCK    = `<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>`;
    const ICON_FAX      = `<path d="M4 4h16v16H4z"/><path d="M8 2h8v4H8z"/><path d="M6 10h12"/><path d="M6 14h12"/>`;
    
    const CARD_THEMES = {
        white: {
            iconBg: "#43A047",
            iconStroke: "#ffffff",
            titleUnderline: "#43A047",
            avatarStroke: "#43A047",
            urlColor: "#33691e",
            timesBoxBg: "rgba(190, 235, 159, 0.15)",
            timesBoxBorder: "rgba(0,0,0,0.15)",
            titleColor: "#1a1a1a",
            textColor: "#2d3748",
            timesTitleColor: "#254618",
            hintColor: "#1a1a1a",
            background: "rgba(255,255,255,1)"
        },
        coffee: {
            iconBg: "#6D4C41",
            iconStroke: "#F5F5F5",
            titleUnderline: "#6D4C41",
            avatarStroke: "#6D4C41",
            urlColor: "#6D4C41",
            timesBoxBg: "rgba(209, 188, 175, 0.25)",
            timesBoxBorder: "#A1887F",
            titleColor: "#4E342E",
            textColor: "#4E342E",
            timesTitleColor: "#4E342E",
            hintColor: "#4E342E",
            background: "rgba(245,245,245,1)"
        },
        yellow: {
            iconBg: "#F9A825",
            iconStroke: "#ffffff",
            titleUnderline: "#F9A825",
            avatarStroke: "#F9A825",
            urlColor: "#F57F17",
            timesBoxBg: "rgba(255, 224, 130, 0.25)",
            timesBoxBorder: "#FBC02D",
            titleColor: "#5D4037",
            textColor: "#4E342E",
            timesTitleColor: "#5D4037",
            hintColor: "#5D4037",
            background: "rgba(255,253,231,1)"
        },
        green: {
            iconBg: "#2E7D32",
            iconStroke: "#ffffff",
            titleUnderline: "#2E7D32",
            avatarStroke: "#2E7D32",
            urlColor: "#1B5E20",
            timesBoxBg: "rgba(200, 230, 201, 0.25)",
            timesBoxBorder: "#66BB6A",
            titleColor: "#1B5E20",
            textColor: "#2E7D32",
            timesTitleColor: "#1B5E20",
            hintColor: "#1B5E20",
            background: "rgba(241,248,233,1)"
        },
        red: {
            iconBg: "#C62828",
            iconStroke: "#ffffff",
            titleUnderline: "#C62828",
            avatarStroke: "#C62828",
            urlColor: "#B71C1C",
            timesBoxBg: "rgba(255, 205, 210, 0.25)",
            timesBoxBorder: "#EF5350",
            titleColor: "#B71C1C",
            textColor: "#C62828",
            timesTitleColor: "#B71C1C",
            hintColor: "#B71C1C",
            background: "rgba(255,235,238,1)"
        },
        blue: {
            iconBg: "#1E88E5",
            iconStroke: "#ffffff",
            titleUnderline: "#1E88E5",
            avatarStroke: "#1E88E5",
            urlColor: "#1565C0",
            timesBoxBg: "rgba(144, 202, 249, 0.25)",
            timesBoxBorder: "#64B5F6",
            titleColor: "#0D47A1",
            textColor: "#1E3A5F",
            timesTitleColor: "#0D47A1",
            hintColor: "#0D47A1",
            background: "rgba(227,242,253,1)"
        }
    };
    
    let TEXT_SHADOW_STYLE = "text-shadow:none;";
    
    function fontWeightFor(type) {
        return FONT.fields[type].bold ? FONT.weight.bold : FONT.weight.normal;
    }
    
    function fontStyleFor(type) {
        return FONT.fields[type].italic ? "italic" : "normal";
    }
    
    async function smartCreateState(id, value, options = {}) {
        if (existsState(id)) return;
        await createState(id, value, options);
    }
    
    function sleepMs(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    function base64Decode(str) {
        return Buffer.from(str, "base64").toString("utf8");
    }
    
    function decodeSampleData() {
        const decoded = base64Decode(SAMPLE_DATA_BASE64);
        return JSON.parse(decoded);
    }
    
    async function processJSON() {
        try {
            const raw = getState(JSON_DP).val;
            if (!raw) return;
    
            const data = JSON.parse(raw);
            if (!data.eintraege || !Array.isArray(data.eintraege)) return;
    
            for (const entry of data.eintraege) {
                const svg = buildSVG(entry);
                const dp = SVG_ROOT + entry.id;
    
                await smartCreateState(dp, "", { type: "string", name: "SVG Karte " + entry.id });
                await sleepMs(100);
    
                setStateIfChanged(dp, svg);
            }
        } catch (err) {
        }
    }
    
    function compressTimes(times) {
        const full    = ["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"];
        const short   = ["Mo","Di","Mi","Do","Fr","Sa","So"];
        const special = {"Feiertage": "Feiertage", "Brückentage": "Brückentage"};
    
        const parsed = times.map(t => {
            const [day, ...rest] = t.split(" ");
            const time = rest.join(" ");
    
            if (special[day]) {
                return { day, short: special[day], time, special: true };
            }
            return {day, short: short[full.indexOf(day)], time, special: false};
        });
    
        const groups = {};
        const specials = [];
    
        parsed.forEach(p => {
            if (p.special) {
                specials.push(`${p.short} ${p.time}`);
            } else {
                if (!groups[p.time]) groups[p.time] = [];
                groups[p.time].push(p.short);
            }
        });
    
        const result = [];
        for (const time in groups) {
            const list = groups[time];
    
            if (list.length === 1) {
                result.push(`${list[0]} ${time}`);
            } else {
                result.push(`${list[0]}–${list[list.length - 1]} ${time}`);
            }
        }
        return result.concat(specials);
    }
    
    function splitTimeRow(row) {
        const parts = row.split(" ");
        const day = parts[0];
        const time = parts.slice(1).join(" ");
        return { day, time };
    }
    
    function splitTimeAtomic(row) {
        const idx = row.indexOf(" ");
        if (idx === -1) return { day: row, parts: [] };
    
        const day  = row.substring(0, idx);
        const rest = row.substring(idx + 1).split(",");
        const parts = [];
    
        rest.forEach(block => {
            const m = block.trim().match(/^(\S+)\s*–\s*(\S+)$/);
            if (m) parts.push({ start: m[1], end: m[2] });
        });
        return { day, parts };
    }
    
    function wrapText(text, maxLen = 32) {
        const words = text.split(" ");
        const lines = [];
        let current = "";
    
        for (const w of words) {
            if ((current + w).length > maxLen) {
                lines.push(current.trim());
                current = w + " ";
            } else {
                current += w + " ";
            }
        }
        if (current.trim().length > 0) lines.push(current.trim());
        return lines;
    }
    
    function normalizeDays(str) {
        const map = {"Montag": "Mo", "Dienstag": "Di", "Mittwoch": "Mi", "Donnerstag": "Do", "Freitag": "Fr", "Samstag": "Sa", "Sonntag": "So"};
        const rangeRegex = /(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)\s*[–-]\s*(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/g;
    
        str = str.replace(rangeRegex, (m, a, b) => `${map[a]}–${map[b]}`);
        for (const full in map) str = str.replace(new RegExp(full, "g"), map[full]);
        return str;
    }
    
    function buildSVG(entry) {
        const DESIGN_PRESET = getState(root+'theme').val;
    
        if (entry.karte === "small") {
            if (DESIGN_PRESET === "standard") return buildSmallCard(entry);
            if (DESIGN_PRESET === "teal")     return buildTealCardSmall(entry);
            if (DESIGN_PRESET === "wave")     return buildWaveCardSmall(entry);
    
            const theme = CARD_THEMES[DESIGN_PRESET] || CARD_THEMES.white;
            return buildUniColorCardSmall(entry, theme);
        }
    
        switch (DESIGN_PRESET) {
            case "teal":     return buildTealCard(entry);
            case "coffee":   return buildCoffeeCard(entry);
            case "wave":     return buildWaveCard(entry);
            case "white":    return buildWhiteCard(entry);
            case "red":      return buildRedCard(entry);
            case "green":    return buildGreenCard(entry);
            case "blue":     return buildBlueCard(entry);
            case "yellow":   return buildYellowCard(entry);
            case "standard":
            default:
                return buildLargeCard(entry);
        }
    }
    
    async function main() {
        await smartCreateState(JSON_DP, "", { type: "string", name: "Kontakte JSON" });
        await smartCreateState(root+'theme', "standard", {
            type: "mixed",
            read: true,
            write: true,
            name: "Card Theme",
            states: {
                "blue":     "Blau",
                "coffee":   "Coffee",
                "yellow":   "Gelb",
                "green":    "Grün",
                "red":      "Rot",
                "standard": "Standard",
                "teal":     "Teal",
                "wave":     "Welle",
                "white":    "Weiß"
            }
        });
        await smartCreateState(root+'textshadow', false, { type: "boolean", name: "Textschatten" });
        await smartCreateState(root+'opacity', 1, { type: "number", name: "Deckkraft" });
        await sleepMs(200);
    
        TEXT_SHADOW_STYLE = getState(root+'textshadow').val ? "text-shadow:3px 3px 4px #000;" : "text-shadow:none;";
    
        on({ id: JSON_DP, change: "any" }, async () => {
            await processJSON();
        });
        on({ id: root+'theme', change: "ne" }, async () => {
            await processJSON();
        });
        on({ id: root+'textshadow', change: "ne" }, async obj => {
            TEXT_SHADOW_STYLE = obj.state.val ? "text-shadow:3px 3px 4px #000;" : "text-shadow:none;";
            await processJSON();
        });
        on({ id: root+'opacity', change: "ne" }, async () => {
            await processJSON();
        });
    
        const current = getState(JSON_DP).val;
        if (!current || current.trim() === "") {
            const sample = decodeSampleData();
            setState(JSON_DP, JSON.stringify(sample, null, 2));
        }
        await processJSON();
    }
    
    main();
    
    function buildLargeCard(e) {
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
        const uid           = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const X = {day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285};
    
    return `
        <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
            <style>
            .txt {
                ${TEXT_SHADOW_STYLE}
                font-family:${FONT.family};
                font-feature-settings:"tnum";
                font-weight: normal;
                font-style: normal;
            }
            </style>
    
            <defs>
                <radialGradient id="glassGlow-${uid}" cx="50%" cy="50%" r="50%">
                    <stop offset="0%" stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                    <stop offset="22%" stop-color="${AVATAR.glow1?.color ?? AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity}"/>
                    <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity}"/>
                    <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity}"/>
                    <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity}"/>
                </radialGradient>
    
                <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                    <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                    <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                    <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
                </radialGradient>
    
                <filter id="glassBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"><feGaussianBlur stdDeviation="12" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
                <filter id="glassShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
    
                <mask id="avatarMask-${uid}"><circle cx="280" cy="115" r="40" fill="white"/></mask>
            </defs>
    
            <circle cx="280" cy="115" r="90" fill="url(#avatarAuraGlow-${uid})" filter="url(#glassBloom-${uid})" opacity="0.78"/>
            <circle cx="280" cy="115" r="62" fill="url(#glassGlow-${uid})" filter="url(#glassBloom-${uid})"/>
            <circle cx="280" cy="115" r="46" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
            <ellipse cx="270" cy="102" rx="12" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 270 102)"/>
            <circle cx="280" cy="115" r="42" fill="${AVATAR.background}" filter="url(#glassShadow-${uid})"/>
            <image href="${e.bild}" x="240" y="75" width="80" height="80" mask="url(#avatarMask-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
            ${(() => {
                const lines = [];
                lines.push({text: e.name, type: "name", y: 40});
                lines.push({text: e.adresse.strasse, type: "address", y: 75});
                lines.push({text: `${e.adresse.plz} ${e.adresse.ort}`, type: "address", y: 93});
    
                const tels = e.kontakt.telefon.split(/[,;]\s*/);
                tels.forEach((tel, i) => lines.push({text: `Tel.: ${tel}`, type: "contact", y: 130 + i * 18}));
    
                if (e.kontakt.fax)   lines.push({text: `Fax: ${e.kontakt.fax}`, type: "contact", y: 148});
                if (e.kontakt.email) lines.push({text: e.kontakt.email, type: "contact", y: 166});
                if (e.kontakt.web)   lines.push({text: e.kontakt.web, type: "web", y: 184});
    
                lines.push({text: "Öffnungszeiten", type: "title", y: 225});
                return lines.map(l => `
                    <text class="txt"
                        x="18"
                        y="${l.y}"
                        fill="${FONT.fields[l.type].color}"
                        font-size="${FONT.fields[l.type].size}"
                        font-weight="${FONT.fields[l.type].bold ? FONT.weight.bold : FONT.weight.normal}"
                        font-style="${FONT.fields[l.type].italic ? "italic" : "normal"}"
                        style="font-size:${FONT.fields[l.type].size}px;">
                        ${l.text}
                    </text>
                `).join("");
            })()}
    
            ${hasValidTimes ? zeiten.map((z,i)=>{
                const {day,parts}=splitTimeAtomic(z);
                const b1=parts[0]||null, b2=parts[1]||null;
                return `
                    <text class="txt"
                        y="${250+i*20}"
                        fill="${FONT.fields.times.color}"
                        font-size="${FONT.fields.times.size}"
                        font-weight="${FONT.fields.times.bold ? FONT.weight.bold : FONT.weight.normal}"
                        font-style="${FONT.fields.times.italic ? "italic" : "normal"}"
                        style="font-size:${FONT.fields.times.size}px;">
                        <tspan x="${X.day}">${day}</tspan>
                        ${b1?`<tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>`:""}
                        ${b2?`<tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>`:""}
                    </text>`;
            }).join("") : `
                <text class="txt"
                    x="18"
                    y="250"
                    fill="${FONT.fields.times.color}"
                    font-size="${FONT.fields.times.size}"
                    font-weight="${FONT.fields.times.bold ? FONT.weight.bold : FONT.weight.normal}"
                    font-style="${FONT.fields.times.italic ? "italic" : "normal"}"
                    style="font-size:${FONT.fields.times.size}px;">
                    ${e.hinweis}
                </text>
            `}
    
            ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>`
                <text class="txt"
                    x="18"
                    y="${250+zeiten.length*20+10+i*14}"
                    fill="${FONT.fields.hint.color}"
                    font-size="${FONT.fields.hint.size}"
                    font-weight="${FONT.fields.hint.bold ? FONT.weight.bold : FONT.weight.normal}"
                    font-style="${FONT.fields.hint.italic ? "italic" : "normal"}"
                    style="font-size:${FONT.fields.hint.size}px;">
                    ${line}
                </text>
            `).join("") : ""}
        </svg>
    `.trim();
    }
    
    function buildSmallCard(e) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const gf     = AVATAR.glowFactorSmall;
        const af     = AVATAR.auraFactorSmall;
        const zeiten = e.zeiten.map(z => normalizeDays(z));
    
    return `
        <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
            <style>
            .txt {
                ${TEXT_SHADOW_STYLE}
                font-family:${FONT.family};
                font-feature-settings:"tnum";
            }
            </style>
    
            <defs>
                <radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%">
                    <stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                    <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/>
                    <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/>
                    <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/>
                    <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/>
                </radialGradient>
    
                <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                    <stop offset="0%"   stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity * af}"/>
                    <stop offset="60%"  stop-color="${AVATAR.aura.mid.color}"   stop-opacity="${AVATAR.aura.mid.opacity * af}"/>
                    <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity * af}"/>
                </radialGradient>
    
                <filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"><feGaussianBlur stdDeviation="8" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
                <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
            </defs>
    
            <circle cx="300" cy="80" r="48" fill="url(#avatarAuraGlow-${uid})" filter="url(#avatarBloom-${uid})" opacity="0.88"/>
            <circle cx="300" cy="80" r="44" fill="url(#avatarCoreGlow-${uid})" filter="url(#avatarBloom-${uid})"/>
            <circle cx="300" cy="80" r="32" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
            <ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/>
            <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/>
            <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
            ${(() => {
                const lines = [];
                lines.push({text: e.name, type: "name", y: 30});
                lines.push({text: `Tel.: ${e.kontakt.telefon}`, type: "contact", y: 55});
                lines.push({text: "Öffnungszeiten", type: "title", y: 78});
    
                return lines.map(l => `
                    <text class="txt"
                        x="18"
                        y="${l.y}"
                        fill="${FONT.fields[l.type].color}"
                        font-size="${FONT.fields[l.type].size}"
                        font-weight="${FONT.fields[l.type].bold ? FONT.weight.bold : FONT.weight.normal}"
                        font-style="${FONT.fields[l.type].italic ? "italic" : "normal"}"
                        style="font-size:${FONT.fields[l.type].size}px;">
                        ${l.text}
                    </text>
                `).join("");
            })()}
    
            ${zeiten.map((z,i)=>{
                const {day,parts}=splitTimeAtomic(z);
                const b1=parts[0]||null, b2=parts[1]||null;
    
                return `
                    <text class="txt" y="${95+i*20}" fill="${FONT.fields.times.color}" font-size="${FONT.fields.times.size}" font-weight="${FONT.fields.times.bold ? FONT.weight.bold : FONT.weight.normal}" font-style="${FONT.fields.times.italic ? "italic" : "normal"}" style="font-size:${FONT.fields.times.size}px;">
                        <tspan x="18">${day}</tspan>
    
                        ${b1?`
                            <tspan x="145" text-anchor="end">${b1.start}</tspan>
                            <tspan x="160" text-anchor="middle">–</tspan>
                            <tspan x="175" text-anchor="start">${b1.end}</tspan>
                        `:""}
    
                        ${b2?`
                            <tspan x="255" text-anchor="end">${b2.start}</tspan>
                            <tspan x="270" text-anchor="middle">–</tspan>
                            <tspan x="285" text-anchor="start">${b2.end}</tspan>
                        `:""}
                    </text>`;
            }).join("")}
        </svg>
    `.trim();
    }
    
    function buildWaveCard(e) {
        const uid = `preset2_${e.id}`;
    
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 150, d1: 160, e1: 170, s2: 255, d2: 265, e2: 275};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
    
        const CONTACT_Y = [
            132, // Telefon
            163, // Fax
            194  // Mail
        ];
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="#bfdbfe"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="#ffffff" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        const opacity = getState(root+'opacity').val;
    
        if (tels.length) contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${tels[0]}`, ...tels.slice(1)] });
        if (e.kontakt.fax) contactBlocks.push({ icon: ICON_FAX,  text: `Fax: ${e.kontakt.fax}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: e.kontakt.email });
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f8fbff"/><stop offset="100%" stop-color="#dcecff"/></linearGradient>
                    <linearGradient id="header-${uid}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#60a5fa"/><stop offset="100%" stop-color="#2563eb"/></linearGradient>
                    <clipPath id="avatarClip-${uid}"><rect x="260" y="65" width="65" height="65" rx="16"/></clipPath>
                </defs>
    
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <path d="M0 24 C70 -4 140 44 220 24 C290 4 320 44 ${CARD_LARGE_W} 14 L${CARD_LARGE_W} 0 L0 0 Z" fill="url(#header-${uid})"/>
    
                <text class="txt" x="22" y="44" fill="#163a70" style="font-size:${FONT.fields.name.size}px !important;">${e.name}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="#3b82f6"/>
                <rect x="255" y="60" width="75" height="75" rx="16" fill="#d9e8ff" stroke="#93c5fd"/>
                <image href="${e.bild}" x="260" y="65" width="65" height="65" preserveAspectRatio="xMidYMid slice" clip-path="url(#avatarClip-${uid})"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="#173d74" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.strasse}</text>
                <text class="txt" x="50" y="101" fill="#173d74" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.plz} ${e.adresse.ort}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
                    const lines = entry.lines || [entry.text];
    
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line, idx) => `<text class="txt" x="50" y="${y + (idx * 14)}" fill="#173d74" style="font-size:${FONT.fields.contact.size}px !important;">${line}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="#2563eb" style="font-size:${FONT.fields.web.size}px !important;">${e.kontakt.web}</text>` : ""}
                <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="#bfdbfe" opacity="0.38" stroke="#93c5fd"/>
    
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="#1d4ed8" style="font-size:${FONT.fields.title.size}px !important;">Öffnungszeiten</text>
    
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(z);
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt" y="${timesBoxY + 55 + i * lineHeight}" fill="#173d74" style="font-size:${FONT.fields.times.size}px !important;">
                        <tspan x="${X.day}">${day}</tspan>
    
                        ${b1 ? `
                            <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                            <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                        ` : ""}
    
                        ${b2 ? `
                            <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                            <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                        ` : ""}
                    </text>`;
                }).join("")}
    
                ${e.hinweis && hasValidTimes
                    ? hinweisLines.map((line, i) => `
                        <text class="txt"
                            x="33"
                            y="${280 + zeiten.length * 20 + 10 + i * 14}"
                            fill="#1a1a1a"
                            font-size="${FONT.fields.hint.size}"
                            font-weight="${FONT.fields.hint.bold ? FONT.weight.bold : FONT.weight.normal}"
                            font-style="${FONT.fields.hint.italic ? "italic" : "normal"}"
                            style="font-size:${FONT.fields.hint.size}px;">
                            ${line}
                        </text>
                    `).join("")
                    : ""}
            </svg>
        `.trim();
    }
    
    function buildWaveCardSmall(e) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const zeiten = e.zeiten.map(z => normalizeDays(z));
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const opacity = getState(root+'opacity').val;
    
        return `
        <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
            <style>
                .txt {
                    ${TEXT_SHADOW_STYLE}
                    font-family:${FONT.family};
                    font-feature-settings:"tnum";
                }
            </style>
    
            <defs>
                <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f8fbff"/><stop offset="100%" stop-color="#dcecff"/></linearGradient>
                <linearGradient id="header-${uid}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#60a5fa"/><stop offset="100%" stop-color="#2563eb"/></linearGradient>
                <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
            </defs>
    
            <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
            <path d="M0 24 C70 -4 140 44 220 24 C290 4 320 44 ${CARD_SMALL_W} 14 L${CARD_SMALL_W} 0 L0 0 Z" fill="url(#header-${uid})"/>
            <circle cx="300" cy="80" r="32" fill="#d9e8ff" stroke="#93c5fd" stroke-width="1.5"/>
            <circle cx="300" cy="80" r="22.4" fill="#d9e8ff" filter="url(#avatarShadow-${uid})"/>
            <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
            <text class="txt" x="18" y="30" fill="#163a70" font-size="${FONT.fields.name.size}" style="font-size:${FONT.fields.name.size}px;">${e.name}</text>
            <text class="txt" x="18" y="55" fill="#173d74" font-size="${FONT.fields.contact.size}" style="font-size:${FONT.fields.contact.size}px;">Tel.: ${e.kontakt.telefon}</text>
            <text class="txt" x="18" y="78" fill="#1d4ed8" font-size="${FONT.fields.title.size}" style="font-size:${FONT.fields.title.size}px;">Öffnungszeiten</text>
    
            ${zeiten.map((z,i)=>{
                const {day,parts}=splitTimeAtomic(z);
                const b1=parts[0]||null, b2=parts[1]||null;
    
                return `
                    <text class="txt" y="${95+i*20}" fill="#173d74" font-size="${FONT.fields.times.size}" style="font-size:${FONT.fields.times.size}px;">
                        <tspan x="${X.day}">${day}</tspan>
    
                        ${b1?`
                            <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                            <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                        `:""}
    
                        ${b2?`
                            <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                            <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                        `:""}
                    </text>
                `;
            }).join("")}
    
        </svg>
        `.trim();
    }
    
    function buildTealCard(e) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 150, d1: 160, e1: 170, s2: 255, d2: 265, e2: 275};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
    
        const CONTACT_Y = [
            132, // Telefon
            163, // Fax
            194  // Mail
        ];
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="#e8ffef"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="#4a7763" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        if (tels.length) contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${tels[0]}`, ...tels.slice(1)] });
        if (e.kontakt.fax) contactBlocks.push({ icon: ICON_FAX,  text: `Fax: ${e.kontakt.fax}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: e.kontakt.email });
    
        const opacity = getState(root+'opacity').val;
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#005a70"/><stop offset="100%" stop-color="#87c3ab"/></linearGradient>
                    <clipPath id="avatarClip-${uid}"><rect x="260" y="65" width="65" height="65" rx="14"/></clipPath>
                    <filter id="blurBG" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="8"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <text class="txt" x="22" y="44" fill="white" style="font-size:${FONT.fields.name.size}px !important;">${e.name}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="white" opacity="0.8"/>
    
                <rect x="255" y="60" width="75" height="75" rx="16" fill="rgba(255, 255, 255, 0.12)" stroke="rgba(255, 255, 255, 0.35)"/>
                <image href="${e.bild}" x="260" y="65" width="65" height="65" clip-path="url(#avatarClip-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="#e8ffef" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.strasse}</text>
                <text class="txt" x="50" y="101" fill="#e8ffef" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.plz} ${e.adresse.ort}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
                    const lines = entry.lines || [entry.text];
    
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line, idx) => `<text class="txt" x="50" y="${y + (idx * 14)}" fill="#e8ffef" style="font-size:${FONT.fields.contact.size}px !important;">${line}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="#e8ffef" style="font-size:${FONT.fields.web.size}px !important;">${e.kontakt.web}</text>` : ""}
    
                <g>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="rgba(232,255,239,0.16)" filter="url(#blurBG)"/>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="none" stroke="rgba(255,255,255,0.45)" stroke-width="2"/>
                </g>
    
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="#e8ffef" style="font-size:${FONT.fields.title.size}px !important;">Öffnungszeiten</text>
    
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(z);
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt" y="${timesBoxY + 55 + i * lineHeight}" fill="#e8ffef" style="font-size:${FONT.fields.times.size}px !important;">
                        <tspan x="${X.day}">${day}</tspan>
    
                        ${b1 ? `
                            <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                            <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                        ` : ""}
    
                        ${b2 ? `
                            <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                            <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                        ` : ""}
                    </text>`;
                }).join("")}
    
                ${e.hinweis && hasValidTimes
                    ? hinweisLines.map((line, i) => `
                        <text class="txt"
                              x="33"
                              y="${280 + zeiten.length * 20 + 10 + i * 14}"
                              fill="#1a1a1a"
                              font-size="${FONT.fields.hint.size}"
                              font-weight="${FONT.fields.hint.bold ? FONT.weight.bold : FONT.weight.normal}"
                              font-style="${FONT.fields.hint.italic ? "italic" : "normal"}"
                              style="font-size:${FONT.fields.hint.size}px;">
                              ${line}
                        </text>
                    `).join("")
                    : ""}
            </svg>
        `.trim();
    }
    
    function buildWhiteCard(e) {
        return buildUniColorCard(e, CARD_THEMES.white);
    }
    
    function buildCoffeeCard(e) {
        return buildUniColorCard(e, CARD_THEMES.coffee);
    }
    
    function buildYellowCard(e) {
        return buildUniColorCard(e, CARD_THEMES.yellow);
    }
    
    function buildGreenCard(e) {
        return buildUniColorCard(e, CARD_THEMES.green);
    }
    
    function buildRedCard(e) {
        return buildUniColorCard(e, CARD_THEMES.red);
    }
    
    function buildBlueCard(e) {
        return buildUniColorCard(e, CARD_THEMES.blue);
    }
    
    function buildUniColorCard(e, theme) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 150, d1: 160, e1: 170, s2: 255, d2: 265, e2: 275};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
        const CONTACT_Y      = [132, 163, 194];
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="${theme.iconBg}"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="${theme.iconStroke}" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        const opacity = getState(root+'opacity').val;
    
        if (tels.length)     contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${tels[0]}`, ...tels.slice(1)] });
        if (e.kontakt.fax)   contactBlocks.push({ icon: ICON_FAX,  text: `Fax: ${e.kontakt.fax}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: e.kontakt.email });
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <clipPath id="avatarClip-${uid}"><circle cx="292" cy="98" r="38"/></clipPath>
                    <filter id="blurUni-${uid}" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="${theme.background}" opacity="${opacity}"/>
                <text class="txt" x="22" y="44" fill="${theme.titleColor}" style="font-size:${FONT.fields.name.size}px !important;">${e.name}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="${theme.titleUnderline}" opacity="0.9"/>
    
                <circle cx="292" cy="98" r="42" fill="none" stroke="${theme.avatarStroke}" stroke-width="2"/>
                <image href="${e.bild}" x="250" y="56" width="84" height="84" preserveAspectRatio="xMidYMid slice" clip-path="url(#avatarClip-${uid})"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="${theme.textColor}" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.strasse}</text>
                <text class="txt" x="50" y="101" fill="${theme.textColor}" style="font-size:${FONT.fields.address.size}px !important;">${e.adresse.plz} ${e.adresse.ort}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
                    const lines = entry.lines || [entry.text];
    
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line, idx) => `<text class="txt" x="50" y="${y + (idx * 14)}" fill="${theme.textColor}" style="font-size:${FONT.fields.contact.size}px !important;">${line}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="${theme.urlColor}" style="font-size:${FONT.fields.web.size}px !important;">${e.kontakt.web}</text>` : ""}
    
                <g>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="${theme.timesBoxBg}" filter="url(#blurUni-${uid})"/>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="none" stroke="${theme.timesBoxBorder}" stroke-width="2"/>
                </g>
    
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="${theme.timesTitleColor}" style="font-size:${FONT.fields.title.size}px !important;">Öffnungszeiten</text>
    
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(z);
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt" y="${timesBoxY + 55 + i * lineHeight}" fill="${theme.textColor}" style="font-size:${FONT.fields.times.size}px !important;">
                        <tspan x="${X.day}">${day}</tspan>
    
                        ${b1 ? `
                            <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                            <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                        ` : ""}
    
                        ${b2 ? `
                            <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                            <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                            <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                        ` : ""}
                    </text>`;
                }).join("")}
    
                ${e.hinweis && hasValidTimes
                    ? hinweisLines.map((line, i) => `
                        <text class="txt"
                              x="33"
                              y="${280 + zeiten.length * 20 + 10 + i * 14}"
                              fill="${theme.hintColor}"
                              font-size="${FONT.fields.hint.size}"
                              font-weight="${FONT.fields.hint.bold ? FONT.weight.bold : FONT.weight.normal}"
                              font-style="${FONT.fields.hint.italic ? "italic" : "normal"}"
                              style="font-size:${FONT.fields.hint.size}px;">
                              ${line}
                        </text>
                    `).join("")
                    : ""}
            </svg>
        `.trim();
    }
    
    function buildTealCardSmall(e) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const zeiten = e.zeiten.map(z => normalizeDays(z));
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const opacity = getState(root+'opacity').val;
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#005a70"/><stop offset="100%" stop-color="#87c3ab"/></linearGradient>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                    <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <circle cx="300" cy="80" r="32" fill="rgba(255,255,255,0.12)" stroke="rgba(255,255,255,0.35)" stroke-width="1.5"/>
                <circle cx="300" cy="80" r="22.4" fill="#0f2647" filter="url(#avatarShadow-${uid})"/>
                <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                <text class="txt" x="18" y="30" fill="white" font-size="${FONT.fields.name.size}" style="font-size:${FONT.fields.name.size}px;">${e.name}</text>
                <text class="txt" x="18" y="55" fill="#e8ffef" font-size="${FONT.fields.contact.size}" style="font-size:${FONT.fields.contact.size}px;">Tel.: ${e.kontakt.telefon}</text>
                <text class="txt" x="18" y="78" fill="#e8ffef" font-size="${FONT.fields.title.size}" style="font-size:${FONT.fields.title.size}px;">Öffnungszeiten</text>
    
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(z);
                    const b1=parts[0]||null, b2=parts[1]||null;
    
                    return `
                        <text class="txt" y="${95+i*20}" fill="#e8ffef" font-size="${FONT.fields.times.size}" style="font-size:${FONT.fields.times.size}px;">
                            <tspan x="${X.day}">${day}</tspan>
    
                            ${b1?`
                                <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                                <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                                <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                            `:""}
    
                            ${b2?`
                                <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                                <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                                <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                            `:""}
                        </text>
                    `;
                }).join("")}
    
            </svg>
        `.trim();
    }
    
    function buildUniColorCardSmall(e, theme) {
        const uid = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const gf  = AVATAR.glowFactorSmall;
        const af  = AVATAR.auraFactorSmall;
    
        const zeiten = e.zeiten.map(z => normalizeDays(z));
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const opacity = getState(root+'opacity').val;
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                    }
                </style>
                
                <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="${theme.background}" opacity="${opacity}"/>
    
                <defs>
                    <radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                        <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/>
                        <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/>
                        <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/>
                        <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/>
                    </radialGradient>
    
                    <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%"   stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity * af}"/>
                        <stop offset="60%"  stop-color="${AVATAR.aura.mid.color}"   stop-opacity="${AVATAR.aura.mid.opacity * af}"/>
                        <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity * af}"/>
                    </radialGradient>
    
                    <filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"><feGaussianBlur stdDeviation="8" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
                    <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                </defs>
    
                <circle cx="300" cy="80" r="32" fill="none" stroke="${theme.avatarStroke}" stroke-width="1.5"/>
                <ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/>
                <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/>
                <image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                <text class="txt" x="18" y="30" fill="${theme.titleColor}" font-size="${FONT.fields.name.size}" font-weight="${fontWeightFor("name")}" font-style="${fontStyleFor("name")}" style="font-size:${FONT.fields.name.size}px;">${e.name}</text>
                <text class="txt" x="18" y="55" fill="${theme.textColor}" font-size="${FONT.fields.contact.size}" font-weight="${fontWeightFor("contact")}" font-style="${fontStyleFor("contact")}">Tel.: ${e.kontakt.telefon}</text>
                <text class="txt" x="18" y="78" fill="${theme.timesTitleColor}" font-size="${FONT.fields.title.size}" font-weight="${fontWeightFor("title")}" font-style="${fontStyleFor("title")}">Öffnungszeiten</text>
    
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(z);
                    const b1=parts[0]||null, b2=parts[1]||null;
    
                    return `
                        <text class="txt" y="${95+i*20}" fill="${theme.textColor}" font-size="${FONT.fields.times.size}" font-weight="${fontWeightFor("times")}" font-style="${fontStyleFor("times")}">
                            <tspan x="${X.day}">${day}</tspan>
    
                            ${b1?`
                                <tspan x="${X.s1}" text-anchor="end">${b1.start}</tspan>
                                <tspan x="${X.d1}" text-anchor="middle">–</tspan>
                                <tspan x="${X.e1}" text-anchor="start">${b1.end}</tspan>
                            `:""}
    
                            ${b2?`
                                <tspan x="${X.s2}" text-anchor="end">${b2.start}</tspan>
                                <tspan x="${X.d2}" text-anchor="middle">–</tspan>
                                <tspan x="${X.e2}" text-anchor="start">${b2.end}</tspan>
                            `:""}
                        </text>
                    `;
                }).join("")}
    
            </svg>
        `.trim();
    }
    
    function setStateIfChanged(id, value) {
        const state = getState(id);
        if (!state) return;
    
        const old = state.val;
        const isObject = v => v !== null && typeof v === "object";
    
        if (isObject(value)) {
            const newStr = JSON.stringify(value);
            let oldStr = null;
    
            if (typeof old === "string") oldStr = old;
            else if (isObject(old)) oldStr = JSON.stringify(old);
    
            if (newStr === oldStr) return;
            return setState(id, newStr, true);
        }
    
        if (old === value) return;
        setState(id, value, true);
    }
    

    Wünsche Euch viel Spaß bei der Umsetzung.

    Ro75.

    EDIT: Mit Stand 31.05.2026 gibt es neue Funktionen.

    CARDS.png

    • Themen: 9 verschiedene Themen stehen via Datenpunkt (theme) zur Auswahl. Standard = standard.
    • Opacity: Die Deckkraft kann gesteuert werden. Standard = 1 (100%).
    • Text-Shadow: Kann nun ebenfalls via Datenpunkt (textshadow) gesteuert werden. Standard = false.

    Zusätzlich können Details zentral konfiguriert werden

    const FONT = {
        family: "InterVariable",   // globale Schriftfamilie
    
        fields: {
            name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
            address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        weight: {
            normal: 400,  // normales Schriftgewicht
            bold:   600   // fettes Schriftgewicht
        }
    };
    

    Sollte selbsterklärend sein.

    sigi234S Online
    sigi234S Online
    sigi234
    Forum Testing Most Active
    schrieb am zuletzt editiert von
    #2

    @Ro75 sagte:

    die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

    und wie?

    Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
    Immer Daten sichern!

    1 Antwort Letzte Antwort
    0
    • Ro75R Online
      Ro75R Online
      Ro75
      schrieb am zuletzt editiert von Ro75
      #3

      mit einem Basic - String (unescaped). Hier VIS1.
      76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg

      Ro75.

      SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

      NegaleinN 1 Antwort Letzte Antwort
      0
      • Ro75R Online
        Ro75R Online
        Ro75
        schrieb am zuletzt editiert von
        #4

        Habe den Code im Eingangspost aktualisiert. Da fehlte eine Funktion.

        Ro75.

        SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

        1 Antwort Letzte Antwort
        0
        • NashraN Offline
          NashraN Offline
          Nashra
          Most Active Forum Testing
          schrieb am zuletzt editiert von
          #5

          Hi, erstmal tolles Skript.
          Aber ich habe ein kleines Problem bei den Zeiten.
          Im json

                "zeiten": [
                  "Montag 08.00 – 12.30, 15.00 – 18.00",
                  "Dienstag 08.00 – 12.30, 15.00 – 18.00",
                  "Mitwoch 08.00 – 12.30",
                  "Donnerstag 08.00 – 12.30, 15.00 – 18.00",
                  "Freitag 08.00 – 12.30"
                ],
          

          Ausgabe
          Screenshot 2026-05-28 130002.png

          Gruß Ralf
          Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

          Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

          1 Antwort Letzte Antwort
          0
          • Ro75R Online
            Ro75R Online
            Ro75
            schrieb am zuletzt editiert von
            #6

            Mittwoch bitte mit 2t, dann klappt das auch.
            Ro75.

            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

            NashraN 1 Antwort Letzte Antwort
            0
            • Ro75R Ro75

              Mittwoch bitte mit 2t, dann klappt das auch.
              Ro75.

              NashraN Offline
              NashraN Offline
              Nashra
              Most Active Forum Testing
              schrieb am zuletzt editiert von
              #7

              @Ro75 sagte:

              Mittwoch bitte mit 2t, dann klappt das auch.
              Ro75.

              🤦 boah, da sucht man die ganze Zeit und übersieht ein "t"
              Danke 😊

              Gruß Ralf
              Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

              Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

              1 Antwort Letzte Antwort
              0
              • Ro75R Online
                Ro75R Online
                Ro75
                schrieb am zuletzt editiert von
                #8

                Ich habe am Script 2 kleine Modifikationen vorgenommen. Ab jetzt kann im Konfigurationsbereich die Schrift und der Textschatten konfiguriert werden.

                const FONT_FAMILY         = "InterVariable";
                const TEXT_SHADOW_ENABLED = true;   // globaler Text‑Shadow an/aus
                

                Diese Einstellungen sind die bisherigen Basiswerte und waren bereits fest definiert. Ab jetzt sind sie konfigurierbar.

                Das Script kann 1:1 aus dem Post#1 übernommen werden. Falls

                const root     = "0_userdata.0.Kontakte."
                

                angepasst wurde, bitte nach Scriptübernahme wieder korrigieren.

                Ro75.

                SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                1 Antwort Letzte Antwort
                1
                • Ro75R Online
                  Ro75R Online
                  Ro75
                  schrieb am zuletzt editiert von
                  #9

                  Es gibt neue Funktionen.

                  CARDS.png

                  • Themen: 9 verschiedene Themen stehen via Datenpunkt (theme) zur Auswahl. Standard = standard.
                  • Opacity: Die Deckkraft kann gesteuert werden. Standard = 1 (100%).
                  • Text-Shadow: Kann nun ebenfalls via Datenpunkt (textshadow) gesteuert werden. Standard = false.

                  Zusätzlich können Details zentral konfiguriert werden

                  const FONT = {
                      family: "InterVariable",   // globale Schriftfamilie
                  
                      fields: {
                          name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
                          address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
                          contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
                          web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
                          title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
                          times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
                          hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
                      },
                  
                      weight: {
                          normal: 400,  // normales Schriftgewicht
                          bold:   600   // fettes Schriftgewicht
                      }
                  };
                  

                  Sollte selbsterklärend sein. Script im Eingangspost #1 aktualisiert.

                  Ro75.

                  SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                  1 Antwort Letzte Antwort
                  1
                  • NashraN Offline
                    NashraN Offline
                    Nashra
                    Most Active Forum Testing
                    schrieb am zuletzt editiert von Nashra
                    #10

                    Kleine Änderung bitte bei den Öffnungszeiten. Die Umrandung ist etwas zu knapp wenn
                    der Text "Termine nach Vereinbarung" angezeigt wird.
                    Sonst super und ein dickes Danke von meiner Frau, die ist happy

                    Edit: Bild vergessen
                    Screenshot 2026-06-01 114620.png

                    Gruß Ralf
                    Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                    Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                    1 Antwort Letzte Antwort
                    0
                    • Ro75R Online
                      Ro75R Online
                      Ro75
                      schrieb am zuletzt editiert von
                      #11

                      Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                      Ro75

                      SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                      NashraN 1 Antwort Letzte Antwort
                      0
                      • Ro75R Ro75

                        Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                        Ro75

                        NashraN Offline
                        NashraN Offline
                        Nashra
                        Most Active Forum Testing
                        schrieb zuletzt editiert von
                        #12

                        @Ro75 sagte:

                        Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                        Ro75

                        Kein Thema, eilt nicht. Habe noch etwas, sag mal, seltsames mit der Anzeige

                              "zeiten": [
                                "Montag 08.00 – 12.30, 14.30 – 18.00",
                                "Dienstag 08.00 – 12.30, 14.30 – 18.00",
                                "Mittwoch 08.00 – 12.30",
                                "Donnerstag 08.00 – 12.30, 14.30 – 18.00",
                                "Freitag 08.00 – 12.30"
                              ],
                        

                        Screenshot 2026-06-01 135218.png

                        anderer Eintrag...

                              "zeiten": [
                                "Montag 08.00 – 11.45, 14.00 – 16.45",
                                "Dienstag 08.00 – 11.45, 14.00 – 16.45",
                                "Mittwoch 08.00 – 11.45",
                                "Donnerstag 08.30 – 11.45, 14.00 – 16.45",
                                "Freitag 08.30 – 11.45"
                              ],
                        

                        Screenshot 2026-06-01 135402.png

                        und dann werden Änderungen bei z.B.

                                times:   { size: 12, color: "#d0d7e2", bold: true, italic: false },  // Öffnungszeiten-Zeilen
                        

                        bei mir nicht übernommen.

                        Gruß Ralf
                        Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                        Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                        1 Antwort Letzte Antwort
                        0
                        • NegaleinN Offline
                          NegaleinN Offline
                          Negalein
                          schrieb zuletzt editiert von
                          #13

                          Servus @ro75

                          Ich steh noch etwas daneben.

                          • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
                          • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
                          und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).

                          • wie bring ich diese Base64 Beispieldaten in verständliches editierbares Format?
                          • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?
                          • wie macht man den En-Dash?

                          Danke

                          ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                          ° Node.js Fixer ---> iob nodejs-update
                          ° Fixer ---> iob fix

                          NashraN 1 Antwort Letzte Antwort
                          0
                          • Ro75R Online
                            Ro75R Online
                            Ro75
                            schrieb zuletzt editiert von Ro75
                            #14

                            Das Script erstellt, sofern es das erste mal gestartet wird die Beispieldaten selbst. Genau dafür wird der Base64 Code benötigt. Du selbst musst damit gar nichts machen.

                            Sobald das Script läuft siehst die den Datenpunkt. Anklicken, ansehen und damit arbeiten. Also anpassen, hinzufügen. Wenn gespeichert wird sofort der SVG Code aktualisiert.

                            Ro75

                            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                            1 Antwort Letzte Antwort
                            1
                            • NashraN Offline
                              NashraN Offline
                              Nashra
                              Most Active Forum Testing
                              schrieb zuletzt editiert von
                              #15

                              Moin @ro75
                              das mit der Anzeige (Uhrzeiten) hat sich erledigt.
                              Es hatten sich Bindestriche eingeschlichen, kam wohl dadurch das ich
                              alles in Notepad++ bearbeitet habe und beim tippen diese dann mit
                              reingehauen hatte. Jetzt wird alles richtig angezeigt.

                              Gruß Ralf
                              Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                              Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                              1 Antwort Letzte Antwort
                              1
                              • NegaleinN Negalein

                                Servus @ro75

                                Ich steh noch etwas daneben.

                                • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
                                • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
                                und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).

                                • wie bring ich diese Base64 Beispieldaten in verständliches editierbares Format?
                                • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?
                                • wie macht man den En-Dash?

                                Danke

                                NashraN Offline
                                NashraN Offline
                                Nashra
                                Most Active Forum Testing
                                schrieb zuletzt editiert von Nashra
                                #16

                                @negalein

                                • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?

                                kommt alles in eine json, d.h. alles im DP editieren

                                • wie macht man den En-Dash?

                                Bei Windows: Alt + 0150 (auf dem Ziffernblock)

                                Gruß Ralf
                                Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                1 Antwort Letzte Antwort
                                0
                                • Ro75R Online
                                  Ro75R Online
                                  Ro75
                                  schrieb zuletzt editiert von Ro75
                                  #17

                                  Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                  Ro75.

                                  SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                                  NashraN 1 Antwort Letzte Antwort
                                  0
                                  • Ro75R Ro75

                                    mit einem Basic - String (unescaped). Hier VIS1.
                                    76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg

                                    Ro75.

                                    NegaleinN Offline
                                    NegaleinN Offline
                                    Negalein
                                    schrieb zuletzt editiert von
                                    #18

                                    @Ro75 sagte:

                                    mit einem Basic - String (unescaped). Hier VIS1.

                                    das funktioniert bei mir nicht

                                    Vis-Edit
                                    Edit-vis-02-06-2026_09_45.png

                                    Vis-Runtime
                                    vis-02-06-2026_09_45.png

                                    ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                                    ° Node.js Fixer ---> iob nodejs-update
                                    ° Fixer ---> iob fix

                                    1 Antwort Letzte Antwort
                                    0
                                    • NashraN Offline
                                      NashraN Offline
                                      Nashra
                                      Most Active Forum Testing
                                      schrieb zuletzt editiert von
                                      #19

                                      @negalein
                                      du mußt die svg Datenpunkte nehmen
                                      Screenshot 2026-06-02 105714.png

                                      Screenshot 2026-06-02 105645.png

                                      Gruß Ralf
                                      Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                      Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                      NegaleinN 1 Antwort Letzte Antwort
                                      0
                                      • Ro75R Ro75

                                        Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                        Ro75.

                                        NashraN Offline
                                        NashraN Offline
                                        Nashra
                                        Most Active Forum Testing
                                        schrieb zuletzt editiert von Nashra
                                        #20

                                        @Ro75 sagte:

                                        Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                        Ro75.

                                        Wie schon geschrieben, keine Eile.

                                        Gruß Ralf
                                        Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                        Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                        1 Antwort Letzte Antwort
                                        0
                                        • NashraN Nashra

                                          @negalein
                                          du mußt die svg Datenpunkte nehmen
                                          Screenshot 2026-06-02 105714.png

                                          Screenshot 2026-06-02 105645.png

                                          NegaleinN Offline
                                          NegaleinN Offline
                                          Negalein
                                          schrieb zuletzt editiert von
                                          #21

                                          @Nashra sagte:

                                          du mußt die svg Datenpunkte nehmen

                                          danke

                                          @ro75

                                          die id1 schaut so aus.
                                          in der view wir vom freitag nichts angezeigt.

                                          und geht es, dass nicht

                                          Mo-Mi
                                          Di-Do

                                          steht, sondern entweder jeder tag extra untereinander, oder zumindest
                                          Mo, Mi
                                          Di, Do

                                              {
                                                "id": 1,
                                                "karte": "large",
                                                "typ": "hausarzt",
                                                "name": "Hausarzt – Dr. Bernhard Hohenberger",
                                                "bild": "https://gruentalpraxis.at/wp-content/uploads/2025/04/GruentalPraxis_LOGO_4c.png",
                                                "adresse": {
                                                  "strasse": "Kenzianweg 9",
                                                  "plz": "4780",
                                                  "ort": "Schärding"
                                                },
                                                "zeiten": [
                                                  "Montag 8:30 – 12:00",
                                                  "Dienstag 15:00 – 19:00",
                                                  "Mittwoch 8:30 – 12:00",
                                                  "Donnerstag 15:00 – 19:00",
                                                  "Freitag 8:30 – 12:00 und (alle 4 Wochen) 13:00 – 16:00"
                                                ],
                                                "kontakt": {
                                                  "telefon": "+43 7712 355 30",
                                                  "email": "office@gruentalpraxis.at",
                                                  "web": "https://gruentalpraxis.at/"
                                                },
                                                "hinweis": ""
                                              },
                                          

                                          776cad60-9770-45a3-a568-c36d4a06e130-image.jpeg

                                          ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                                          ° Node.js Fixer ---> iob nodejs-update
                                          ° Fixer ---> iob fix

                                          1 Antwort Letzte Antwort
                                          0

                                          Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                                          Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                                          Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                                          Registrieren Anmelden
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          603

                                          Online

                                          32.9k

                                          Benutzer

                                          83.1k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2026
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe