import { VORGABE_FARBE } from './einstellungen.js' /** * Einbau des Terminplaners auf der Website des Kunden: Bausteine fuer Einbau-Code und Farbvorschlag * (Spezifikation 2026-10-03-terminplaner-tp2-verkauf-design.md, Abschnitt Einbauanleitung; Anleitung fuer das Team: * docs/funktionen/termine-einbau.md, Abschnitte 1 und 3). * * Reine Funktionen ohne Appwrite und ohne config: die Adresse des Portals (`apiBase`) und alle Werte kommen als Parameter. * Der Einbau-Code stand bis TP2 in routes/termine.js; GET /api/termine/einstellungen ruft ihn von hier auf (Modus inline, die Kundenansicht bleibt unveraendert). * * Die Farbfunktionen (luminanz, textFarbeAuf) sind 1:1 die aus public/embed/termine.js (Marken @farben-anfang/@farben-ende), * bewusst kopiert und nicht importiert: das Widget ist ein eigenstaendiges Browser-Skript. test/termineEmbedStatic.test.js * haelt beide fuer 4096 Farben gleich; wer eine Seite aendert, aendert die andere mit. */ const FARBE_RE = /^#[0-9a-fA-F]{6}$/ const KNOPF_MAX = 40 const MIN_RAHMEN = 3 // WCAG 2.1 AA fuer Bedienelemente und Rahmen (1.4.11): der Rahmen freier Tage steht in der Akzentfarbe // ---------------------------------------------------------------- Text /** Maskiert &, <, >, " fuer Text und Attributwerte in doppelten Anfuehrungszeichen. null und undefined werden zu ''. */ export function esc(text) { return String(text ?? '').replace(/[&<>"]/g, (z) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[z]) } /** Knopf-Text fuer data-knopf: Zeilenumbrueche und Steuerzeichen zu Leerzeichen, hoechstens 40 Zeichen (nie mitten in einem Zeichen). */ function knopfTextBereinigt(roh) { if (typeof roh !== 'string') return '' const eine = roh.replace(/\r\n/g, ' ').replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, ' ').trim() return Array.from(eine).slice(0, KNOPF_MAX).join('') } // ---------------------------------------------------------------- Einbau-Code /** * Einbau-Code fuer die Website: Platzhalter mit Ausweichtext (bleibt ohne JavaScript stehen) und das Skript. * modus: 'inline' (Standard, die Strecke steht in der Seite) | 'knopf' (ein Knopf oeffnet einen Dialog); jeder andere Wert gilt * als 'inline'. knopfText gilt nur im Modus knopf und nur, wenn er nicht leer ist; sonst nimmt das Widget "Termin buchen". */ export function einbauSnippet({ siteKey, apiBase, telefon, modus = 'inline', knopfText } = {}) { const knopf = modus === 'knopf' ? knopfTextBereinigt(knopfText) : '' const ausweich = telefon ? `Termine telefonisch unter ${esc(telefon)}.` : 'Termine telefonisch vereinbaren.' const attribute = [ `data-webklar-termine="${esc(siteKey)}"`, `data-api="${esc(apiBase)}"`, `data-modus="${modus === 'knopf' ? 'knopf' : 'inline'}"`, ...(knopf ? [`data-knopf="${esc(knopf)}"`] : []), ] return [ `
`, `

${ausweich}

`, '
', ``, ].join('\n') } // ---------------------------------------------------------------- Farben /** Relative Leuchtdichte nach WCAG 2.1 fuer '#rrggbb' (wie im Widget; erwartet eine gueltige Farbe). */ export function luminanz(farbe) { const rgb = [1, 3, 5].map((i) => { const c = parseInt(farbe.slice(i, i + 2), 16) / 255 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) }) return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] } /** Kontrastverhaeltnis zweier Leuchtdichten (1 bis 21), wie kontrast() im Widget. */ const verhaeltnis = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05) /** Kontrastverhaeltnis zweier Farben '#rrggbb' (1 bis 21, Reihenfolge egal). */ export function kontrast(hexA, hexB) { return verhaeltnis(luminanz(hexA), luminanz(hexB)) } /** * Schrift auf der Betriebsfarbe: Weiss oder das dunkle Grau, je nachdem was mehr Kontrast gibt; erreicht keines 4,5 : 1 * (mittlere Toene), nimmt sie Schwarz (mindestens 4,58 : 1 fuer jede Farbe). 1:1 aus dem Widget. */ export function textFarbeAuf(farbe) { const l = luminanz(farbe) const weiss = verhaeltnis(l, 1) const dunkel = verhaeltnis(l, luminanz('#111827')) const beste = Math.max(weiss, dunkel) if (beste < 4.5 && verhaeltnis(l, 0) > beste) return '#000000' return weiss >= dunkel ? '#ffffff' : '#111827' } /** Kontrast mit einer Nachkommastelle und deutschem Komma, abgerundet ("unter 3 : 1" bleibt auch im Text unter 3). */ const kontrastAnzeige = (wert) => (Math.floor(wert * 10) / 10).toFixed(1).replace('.', ',') /** * CSS-Vorschlag fuer die Website aus der Betriebsfarbe (Anleitung Abschnitt 3): Kommentar plus * `[data-webklar-termine]{ --wkt-akzent; --wkt-akzent-text }`. Nur eine gueltige '#rrggbb' (gross oder klein) wird uebernommen; * alles andere ersetzt VORGABE_FARBE, der Rohtext kommt nie in die Ausgabe. * Rueckgabe: css, akzent (die verwendete Farbe, klein), akzentText (Schrift darauf), kontrastText (Akzent gegen Schrift), * kontrastSeite (Akzent gegen Weiss), hinweise (Warnungen, leer wenn alles passt). Der Rahmen der freien Tage steht in der * Akzentfarbe und braucht gegen den Hintergrund mindestens 3 : 1; das Widget hat standardmaessig einen weissen Hintergrund. */ export function cssVorschlag(farbe) { const akzent = typeof farbe === 'string' && FARBE_RE.test(farbe) ? farbe.toLowerCase() : VORGABE_FARBE const akzentText = textFarbeAuf(akzent) const textKontrast = kontrast(akzent, akzentText) const seitenKontrast = kontrast(akzent, '#ffffff') const hinweise = [] if (seitenKontrast < MIN_RAHMEN) { hinweise.push( `Kontrast gegen Weiß: nur ${kontrastAnzeige(seitenKontrast)} : 1. Der Rahmen der freien Tage steht in der Akzentfarbe ${akzent} und braucht ` + `mindestens ${MIN_RAHMEN} : 1 gegen den Hintergrund. Auf weißem Grund wähle eine dunklere Farbe, auf dunklem Grund setze --wkt-hintergrund passend.` ) } const css = [ `/* Terminplaner: Farbe der Knöpfe und Schrift darauf, aus der Betriebsfarbe. Kontrast der Schrift: ${kontrastAnzeige(textKontrast)} : 1. */`, '[data-webklar-termine]{', ` --wkt-akzent:${akzent};`, ` --wkt-akzent-text:${akzentText};`, '}', ].join('\n') return { css, akzent, akzentText, kontrastText: textKontrast, kontrastSeite: seitenKontrast, hinweise } }