From a071e57fb71397fc695deaf29dc25d5bdd451b69 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 15:20:27 +0000 Subject: [PATCH] =?UTF-8?q?Buchungskalender:=20Abschluss=20=E2=80=93=20Wid?= =?UTF-8?q?get-CSS=20ohne=20@layer,=20alles=20unter=20.wku-root,=20Reset,?= =?UTF-8?q?=20Fenster=20600=20Tage,=20Serverfehlertexte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- public/embed/unterkunft.js | 124 +++++++++++++++++------------ test/unterkunftEmbedStatic.test.js | 18 ++++- test/unterkunftWidgetFluss.test.js | 18 +++++ 3 files changed, 105 insertions(+), 55 deletions(-) diff --git a/public/embed/unterkunft.js b/public/embed/unterkunft.js index eee06c9..edad1e8 100644 --- a/public/embed/unterkunft.js +++ b/public/embed/unterkunft.js @@ -79,63 +79,63 @@ /* ------------------------------------------------------------------- Gestaltung --- */ var CSS = [ - '@layer wku{', '.wku-root{--wku-i-akzent:var(--wku-akzent,var(--wku-vorgabe-akzent,#0f766e));--wku-i-akzent-text:var(--wku-akzent-text,var(--wku-vorgabe-akzent-text,#fff));', '--wku-i-bg:var(--wku-hintergrund,#fff);--wku-i-text:var(--wku-text,#1f2937);--wku-i-rand:var(--wku-rand,#b6bcc6);--wku-i-radius:var(--wku-radius,12px);', '--wku-i-weich:#f3f4f6;--wku-i-bereich:#e5eceb;--wku-i-fehler:#b91c1c;--wku-i-schrift:var(--wku-schrift,inherit);--wku-i-titel:var(--wku-schrift-titel,var(--wku-schrift,inherit))}', '@supports (color:color-mix(in srgb,red,blue)){.wku-root{--wku-i-weich:color-mix(in srgb,var(--wku-i-text) 6%,var(--wku-i-bg));--wku-i-bereich:color-mix(in srgb,var(--wku-i-akzent) 18%,var(--wku-i-bg))}}', - '.wku-root{container:wku/inline-size;display:block;position:relative;box-sizing:border-box;color:var(--wku-i-text);font-family:var(--wku-i-schrift);font-size:16px;line-height:1.5;text-align:left}', + '.wku-root{container:wku/inline-size;display:block;position:relative;box-sizing:border-box;color:var(--wku-i-text);font-family:var(--wku-i-schrift);font-size:16px;line-height:1.5;text-align:left;width:100%;max-width:100%}', + '.wku-root :is(button,input,select,textarea,label,h1,h2,h3,h4,h5,h6,p,a,fieldset,legend){all:revert;box-sizing:border-box}', + '.wku-root [hidden]{display:none}', '.wku-root *,.wku-root *::before,.wku-root *::after{box-sizing:border-box}', - '.wku-vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}', - '.wku-haupt{display:grid;grid-template-columns:minmax(0,1fr);gap:24px}', - '.wku-karte{background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:var(--wku-i-radius);padding:20px}', - '.wku-titel{font-family:var(--wku-i-titel);font-size:1.2rem;line-height:1.25;font-weight:700;margin:0 0 .75rem}', - '.wku-ausweich{margin:0}', - '.wku-monate{display:grid;grid-template-columns:minmax(0,1fr);gap:20px}', - '.wku-zwei{display:none}', - '.wku-monat-kopf{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:44px;margin-bottom:.35rem}', - '.wku-monat-name{font-family:var(--wku-i-titel);font-size:1.05rem;font-weight:700;margin:0;flex:1;text-align:center}', - '.wku-pfeil{font:inherit;font-size:1.4rem;line-height:1;min-width:44px;min-height:44px;border-radius:50%;border:1px solid var(--wku-i-rand);background:var(--wku-i-bg);color:inherit;cursor:pointer;padding:0}', - '.wku-pfeil:hover{background:var(--wku-i-weich)}.wku-pfeil[disabled]{opacity:.3;cursor:default}.wku-pfeil-leer{display:inline-block;width:44px;height:44px}', - '.wku-raster{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));row-gap:2px}', - '.wku-wt{font-size:.78rem;font-weight:600;text-align:center;padding:.25rem 0;opacity:.7}', - '.wku-leer{min-height:44px}', - '.wku-tag{font:inherit;position:relative;min-height:44px;padding:0;margin:0;border:0;border-radius:0;background:transparent;color:inherit;text-align:center;cursor:pointer;overflow:hidden}', - '.wku-tag>span{position:relative;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;border:1px solid transparent;z-index:1}', - '.wku-tag:hover>span{background:var(--wku-i-weich)}', - '.wku-heute>span{border-color:var(--wku-i-text)}', - '.wku-vergangen,.wku-zu{opacity:.35;cursor:default}.wku-vergangen:hover>span,.wku-zu:hover>span{background:transparent}', - '.wku-belegt{color:#6b7280;cursor:default;background:linear-gradient(to top right,transparent calc(50% - 1px),#9ca3af calc(50% - 1px),#9ca3af calc(50% + 1px),transparent calc(50% + 1px)) center/60% 60% no-repeat}', - '.wku-belegt:hover>span{background:transparent}', - '.wku-bereich{background:var(--wku-i-bereich)}', - '.wku-an{background:linear-gradient(to right,transparent 50%,var(--wku-i-bereich) 50%)}', - '.wku-ab{background:linear-gradient(to left,transparent 50%,var(--wku-i-bereich) 50%)}', - '.wku-an.wku-ab{background:transparent}', - '.wku-an>span,.wku-ab>span,.wku-an:hover>span,.wku-ab:hover>span{background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);border-color:var(--wku-i-akzent)}', + '.wku-root .wku-vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}', + '.wku-root .wku-haupt{display:grid;grid-template-columns:minmax(0,1fr);gap:24px}', + '.wku-root .wku-karte{background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:var(--wku-i-radius);padding:20px}', + '.wku-root .wku-titel{font-family:var(--wku-i-titel);font-size:1.2rem;line-height:1.25;font-weight:700;margin:0 0 .75rem}', + '.wku-root.wku-ausweich{margin:0}', + '.wku-root .wku-monate{display:grid;grid-template-columns:minmax(0,1fr);gap:20px}', + '.wku-root .wku-zwei{display:none}', + '.wku-root .wku-monat-kopf{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:44px;margin-bottom:.35rem}', + '.wku-root .wku-monat-name{font-family:var(--wku-i-titel);font-size:1.05rem;font-weight:700;margin:0;flex:1;text-align:center}', + '.wku-root .wku-pfeil{font:inherit;font-size:1.4rem;line-height:1;min-width:44px;min-height:44px;border-radius:50%;border:1px solid var(--wku-i-rand);background:var(--wku-i-bg);color:inherit;cursor:pointer;padding:0}', + '.wku-root .wku-pfeil:hover{background:var(--wku-i-weich)}.wku-root .wku-pfeil[disabled]{opacity:.3;cursor:default}.wku-root .wku-pfeil-leer{display:inline-block;width:44px;height:44px}', + '.wku-root .wku-raster{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));row-gap:2px}', + '.wku-root .wku-wt{font-size:.78rem;font-weight:600;text-align:center;padding:.25rem 0;opacity:.7}', + '.wku-root .wku-leer{min-height:44px}', + '.wku-root .wku-tag{font:inherit;position:relative;min-height:44px;padding:0;margin:0;border:0;border-radius:0;background:transparent;color:inherit;text-align:center;cursor:pointer;overflow:hidden}', + '.wku-root .wku-tag>span{position:relative;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;border:1px solid transparent;z-index:1}', + '.wku-root .wku-tag:hover>span{background:var(--wku-i-weich)}', + '.wku-root .wku-heute>span{border-color:var(--wku-i-text)}', + '.wku-root .wku-vergangen,.wku-root .wku-zu{opacity:.35;cursor:default}.wku-root .wku-vergangen:hover>span,.wku-root .wku-zu:hover>span{background:transparent}', + '.wku-root .wku-belegt{color:#6b7280;cursor:default;background:linear-gradient(to top right,transparent calc(50% - 1px),#9ca3af calc(50% - 1px),#9ca3af calc(50% + 1px),transparent calc(50% + 1px)) center/60% 60% no-repeat}', + '.wku-root .wku-belegt:hover>span{background:transparent}', + '.wku-root .wku-bereich{background:var(--wku-i-bereich)}', + '.wku-root .wku-an{background:linear-gradient(to right,transparent 50%,var(--wku-i-bereich) 50%)}', + '.wku-root .wku-ab{background:linear-gradient(to left,transparent 50%,var(--wku-i-bereich) 50%)}', + '.wku-root .wku-an.wku-ab{background:transparent}', + '.wku-root .wku-an>span,.wku-root .wku-ab>span,.wku-root .wku-an:hover>span,.wku-root .wku-ab:hover>span{background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);border-color:var(--wku-i-akzent)}', '.wku-root :focus-visible{outline:3px solid var(--wku-i-text);outline-offset:2px}', - '.wku-legende{display:flex;flex-wrap:wrap;gap:.25rem 1rem;margin:.75rem 0 0;font-size:.8rem;opacity:.8}', - '.wku-zus{margin:1rem 0 0;font-weight:700;min-height:1.5rem}', - '.wku-hinweis{margin:.25rem 0 0;font-size:.9rem;color:var(--wku-i-fehler);font-weight:600;min-height:1.4rem}', - '.wku-form{margin:0}', - '.wku-feld{margin:0 0 .9rem}', - '.wku-label{display:block;font-weight:600;font-size:.92rem;margin-bottom:.25rem}', - '.wku-stern{color:var(--wku-i-fehler)}', - '.wku-eingabe{display:block;width:100%;min-height:44px;font:inherit;color:inherit;background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:calc(var(--wku-i-radius) * .7);padding:.5rem .7rem}', - 'textarea.wku-eingabe{min-height:5.5rem;resize:vertical}', - '.wku-eingabe[aria-invalid="true"]{border-color:var(--wku-i-fehler);border-width:2px}', - '.wku-feldfehler{color:var(--wku-i-fehler);font-weight:600;font-size:.88rem;margin:.25rem 0 0}', - '.wku-reihe{display:grid;grid-template-columns:minmax(0,1fr);gap:0 12px}', - '.wku-fehler{margin:0 0 .75rem;padding:.6rem .8rem;border-left:4px solid var(--wku-i-fehler);background:var(--wku-i-weich);font-weight:600}', - '.wku-datenschutz{font-size:.85rem;margin:0 0 1rem;opacity:.9}.wku-datenschutz a{color:inherit}', - '.wku-btn{font:inherit;font-weight:700;min-height:48px;padding:.6rem 1.5rem;border-radius:var(--wku-i-radius);border:2px solid var(--wku-i-akzent);background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);cursor:pointer;line-height:1.25;width:100%}', - '.wku-btn:hover{filter:brightness(1.08)}.wku-btn[disabled]{opacity:.6;cursor:default}', - '.wku-danke{margin:0;font-size:1.05rem;font-weight:600}', - '.wku-hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}', - '@container wku (min-width:520px){.wku-reihe{grid-template-columns:1fr 1fr}}', - '@container wku (min-width:760px){.wku-haupt{grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);align-items:start}}', - '@container wku (min-width:1100px){.wku-zwei{display:block}.wku-monate{grid-template-columns:1fr 1fr}.wku-weiter-ein{display:none}}', - '@media (prefers-reduced-motion:no-preference){.wku-btn,.wku-tag>span{transition:background-color .15s}}', - '}' + '.wku-root .wku-legende{display:flex;flex-wrap:wrap;gap:.25rem 1rem;margin:.75rem 0 0;font-size:.8rem;opacity:.8}', + '.wku-root .wku-zus{margin:1rem 0 0;font-weight:700;min-height:1.5rem}', + '.wku-root .wku-hinweis{margin:.25rem 0 0;font-size:.9rem;color:var(--wku-i-fehler);font-weight:600;min-height:1.4rem}', + '.wku-root .wku-form{margin:0}', + '.wku-root .wku-feld{margin:0 0 .9rem}', + '.wku-root .wku-label{display:block;font-weight:600;font-size:.92rem;margin-bottom:.25rem}', + '.wku-root .wku-stern{color:var(--wku-i-fehler)}', + '.wku-root .wku-eingabe{display:block;width:100%;min-height:44px;font:inherit;color:inherit;background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:calc(var(--wku-i-radius) * .7);padding:.5rem .7rem}', + '.wku-root textarea.wku-eingabe{min-height:5.5rem;resize:vertical}', + '.wku-root .wku-eingabe[aria-invalid="true"]{border-color:var(--wku-i-fehler);border-width:2px}', + '.wku-root .wku-feldfehler{color:var(--wku-i-fehler);font-weight:600;font-size:.88rem;margin:.25rem 0 0}', + '.wku-root .wku-reihe{display:grid;grid-template-columns:minmax(0,1fr);gap:0 12px}', + '.wku-root .wku-fehler{margin:0 0 .75rem;padding:.6rem .8rem;border-left:4px solid var(--wku-i-fehler);background:var(--wku-i-weich);font-weight:600}', + '.wku-root .wku-datenschutz{font-size:.85rem;margin:0 0 1rem;opacity:.9}.wku-root .wku-datenschutz a{color:inherit}', + '.wku-root .wku-btn{font:inherit;font-weight:700;min-height:48px;padding:.6rem 1.5rem;border-radius:var(--wku-i-radius);border:2px solid var(--wku-i-akzent);background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);cursor:pointer;line-height:1.25;width:100%}', + '.wku-root .wku-btn:hover{filter:brightness(1.08)}.wku-root .wku-btn[disabled]{opacity:.6;cursor:default}', + '.wku-root .wku-danke{margin:0;font-size:1.05rem;font-weight:600}', + '.wku-root .wku-hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}', + '@container wku (min-width:520px){.wku-root .wku-reihe{grid-template-columns:1fr 1fr}}', + '@container wku (min-width:760px){.wku-root .wku-haupt{grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);align-items:start}}', + '@container wku (min-width:1100px){.wku-root .wku-zwei{display:block}.wku-root .wku-monate{grid-template-columns:1fr 1fr}.wku-root .wku-weiter-ein{display:none}}', + '@media (prefers-reduced-motion:no-preference){.wku-root .wku-btn,.wku-root .wku-tag>span{transition:background-color .15s}}' ].join('') function ensureStyle() { @@ -411,7 +411,7 @@ var jetzt = new Date() var von = jetzt.getUTCFullYear() + '-' + pad(jetzt.getUTCMonth() + 1) + '-' + pad(jetzt.getUTCDate()) von = plusTage(von, -1) - var bis = plusTage(von, 540) + var bis = plusTage(von, 600) return holen(basis + '/belegung?unterkunft=' + encodeURIComponent(unterk.id) + '&von=' + von + '&bis=' + bis, { headers: { Accept: 'application/json' } }).then(function (r) { if (nr !== ladeNr) return if (!r.ok || !r.body.heute || !r.body.bisMax) throw new Error('belegung') @@ -459,8 +459,26 @@ } var b = r.body || {} var gezeigt = false - if (b.felder) Object.keys(b.felder).forEach(function (k) { if (Object.prototype.hasOwnProperty.call(b.felder, k) && feldFehler(k, String(b.felder[k]))) gezeigt = true }) + var uebrig = [] + if (b.felder) Object.keys(b.felder).forEach(function (k) { + if (!Object.prototype.hasOwnProperty.call(b.felder, k)) return + if (feldFehler(k, String(b.felder[k]))) gezeigt = true + else if (k === 'zeitraum' || k === 'personen') uebrig.push(String(b.felder[k])) + }) + if (uebrig.length) { zeigeFehler(uebrig.join(' ')); gezeigt = true } + var fehlerFokus = form.querySelector('[aria-invalid="true"]') + if (fehlerFokus && fehlerFokus.focus) fehlerFokus.focus() if (gezeigt) return + if (b.fehler === 'neu_laden') { + zeigeFehler('Das Formular war zu lange geöffnet. Bitte senden Sie Ihre Anfrage noch einmal ab.') + holen(basis, { headers: { Accept: 'application/json' } }).then(function (c) { if (c.ok && c.body.formToken) token = c.body.formToken }, function () {}) + return + } + if (b.fehler === 'mindestnaechte') { + var mn = parseInt((b.felder && b.felder.mindestNaechte) || unterk.mindestNaechte, 10) || 1 + zeigeFehler('Mindestaufenthalt: ' + (mn === 1 ? '1 Nacht' : mn + ' Nächte') + '. Bitte wählen Sie einen längeren Zeitraum.') + return + } if (b.fehler === 'belegt' || r.status === 409) { zeigeFehler('Dieser Zeitraum ist leider nicht mehr frei. Bitte wählen Sie einen anderen.'); laden().then(null, function () {}) } else if (r.status === 429) zeigeFehler('Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.') else { diff --git a/test/unterkunftEmbedStatic.test.js b/test/unterkunftEmbedStatic.test.js index 4787575..a31c7a1 100644 --- a/test/unterkunftEmbedStatic.test.js +++ b/test/unterkunftEmbedStatic.test.js @@ -17,9 +17,8 @@ test('kein innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new F assert.doesNotMatch(OHNE_KOMMENTARE, /innerHTML|outerHTML|insertAdjacentHTML|document\.write|\beval\s*\(|new Function/) }) -test('API-Pfad, Praefix .wku-, @layer wku, Style-Id, kein Storage, keine Cookies, keine Fremdadressen', () => { +test('API-Pfad, Praefix .wku-, Style-Id, kein Storage, keine Cookies, keine Fremdadressen', () => { assert.ok(OHNE_KOMMENTARE.includes('/api/public/unterkunft/v1/')) - assert.ok(OHNE_KOMMENTARE.includes('@layer wku')) assert.ok(OHNE_KOMMENTARE.includes('wku-style')) assert.match(OHNE_KOMMENTARE, /\.wku-root/) assert.doesNotMatch(OHNE_KOMMENTARE, /localStorage|sessionStorage|document\.cookie|https?:\/\//) @@ -29,3 +28,18 @@ test('Pflichttexte und Honeypot', () => { for (const t of ['Anfragen gerne per Telefon oder E-Mail.', 'Bitte wählen Sie Anreise und Abreise im Kalender.', 'Anfrage senden', 'kontakt_url', 'formToken']) assert.ok(QUELLE.includes(t), t) assert.match(QUELLE, /Danke! Ihre Anfrage/) }) + +// F1 (Abschluss): kein @layer (Seitenregeln wuerden gewinnen), jede Regel unter .wku-root, Reset der Formularelemente. +test('CSS-Isolation: kein @layer, jeder Selektor beginnt mit .wku-root, all:revert fuer Elemente, Fenster +600 Tage', () => { + assert.doesNotMatch(OHNE_KOMMENTARE, /@layer/) + const m = QUELLE.match(/var CSS = \[([\s\S]*?)\]\.join\(''\)/) + assert.ok(m) + const css = [...m[1].matchAll(/'((?:[^'\\]|\\.)*)'/g)].map((x) => x[1]).join('') + const flach = css.replace(/@(container|media|supports)[^{]*\{/g, '') + const regeln = [...flach.matchAll(/([^{}]+)\{[^{}]*\}/g)].map((x) => x[1]) + assert.ok(regeln.length > 40) + for (const sel of regeln) for (const teil of sel.split(/,(?![^(]*\))/)) assert.match(teil.trim(), /^\.wku-root(?![\w-])/, teil) + assert.match(css, /\.wku-root :is\(button,input,select,textarea[^)]*\)\{all:revert/) + assert.match(css, /\.wku-root\{[^}]*width:100%/) + assert.ok(OHNE_KOMMENTARE.includes('plusTage(von, 600)')) +}) diff --git a/test/unterkunftWidgetFluss.test.js b/test/unterkunftWidgetFluss.test.js index 47b84a5..e01bc8b 100644 --- a/test/unterkunftWidgetFluss.test.js +++ b/test/unterkunftWidgetFluss.test.js @@ -90,3 +90,21 @@ test('Monatsnavigation: erster Monat hat aktiven Weiter-Knopf, Fokus bleibt, Tab assert.doesNotMatch(tag(fx, el, '2026-11-22').getAttribute('aria-label'), /Abreisetag möglich/) } finally { fx.restore() } }) + +test('Serverfehler: felder.zeitraum, neu_laden und mindestnaechte erscheinen mit eigenem Text', async () => { + const faelle = [ + [{ status: 400, body: { fehler: 'ungueltig', felder: { zeitraum: 'Ungültiger Zeitraum.' } } }, /Ungültiger Zeitraum\./], + [{ status: 400, body: { fehler: 'neu_laden' } }, /zu lange geöffnet/], + [{ status: 400, body: { fehler: 'mindestnaechte', felder: { mindestNaechte: 3 } } }, /Mindestaufenthalt: 3 Nächte\./], + ] + for (const [antwort, muster] of faelle) { + const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }, antwort, { status: 200, body: CFG }]) + try { + await fx.ruhe(); await fx.ruhe() + await fx.setze(fx.finde(el, '[name="name"]'), 'Max'); await fx.setze(fx.finde(el, '[name="email"]'), 'max@example.com') + await fx.klick(tag(fx, el, '2026-11-10')); await fx.klick(tag(fx, el, '2026-11-14')) + await fx.klick(fx.finde(el, 'button[type="submit"]')) + assert.match(fx.text(el), muster) + } finally { fx.restore() } + } +})