diff --git a/public/auftrag.css b/public/auftrag.css new file mode 100644 index 0000000..dee3b6e --- /dev/null +++ b/public/auftrag.css @@ -0,0 +1,132 @@ +/* Bestätigungsseite für Auftragsbestätigungen (auftrag.html) und Abschnitt „Aufträge“ im Portal; Farben wie die Mailvorlagen. + Die Datei wird auch vom Dashboard geladen: jede Regel ist an body.ab-seite bzw. .ab-portal gebunden, Ausnahmen sind + .ab-rechnungen-titel und .inv-project am Ende. */ + +/* ------------------------------------------------------------------ Farben --- */ +body.ab-seite { + --ab-grund: #f1f0ec; --ab-karte: #ffffff; --ab-text: #111111; --ab-weich: #4a4843; --ab-linie: #e8e6e0; --ab-box: #f7f6f3; + --ab-knopf: #111111; --ab-knopf-text: #ffffff; --ab-fokus: #5a2a91; + --ab-warn-bg: #efe6fb; --ab-warn-text: #5a2a91; --ab-ok-bg: #e4f2e6; --ab-ok-text: #1d5b2c; + --ab-rot-bg: #fbe4e1; --ab-rot-text: #8c1d18; --ab-muted-bg: #ecebe6; --ab-muted-text: #4a4843; --ab-fehler: #b3261e; + color-scheme: light dark; +} +@media (prefers-color-scheme: dark) { + body.ab-seite { + --ab-grund: #161615; --ab-karte: #1f1f1e; --ab-text: #f2f1ee; --ab-weich: #a8a6a0; --ab-linie: #3a3a37; --ab-box: #2a2a28; + --ab-knopf: #f2f1ee; --ab-knopf-text: #161615; --ab-fokus: #d9c2f5; + --ab-warn-bg: #2d2140; --ab-warn-text: #d9c2f5; --ab-ok-bg: #1e3322; --ab-ok-text: #a9dcb3; + --ab-rot-bg: #3d1f1c; --ab-rot-text: #f4b5ae; --ab-muted-bg: #2d2d2b; --ab-muted-text: #a8a6a0; --ab-fehler: #ff8a80; + } +} + +/* ------------------------------------------------------------------ Grundlayout --- */ +body.ab-seite { + margin: 0; min-height: 100dvh; display: flex; flex-direction: column; + background: var(--ab-grund); color: var(--ab-text); + font: 16px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-text-size-adjust: 100%; +} +.ab-seite .hidden { display: none !important; } +.ab-seite .ab-innen { max-width: 40rem; margin: 0 auto; padding: 0 16px; box-sizing: border-box; width: 100%; } +.ab-seite .ab-kopf { padding: max(20px, env(safe-area-inset-top)) 0 12px; } +.ab-seite .ab-kopf .ab-innen { display: flex; align-items: center; gap: 10px; } +.ab-seite .ab-logo { width: 36px; height: 36px; border-radius: 10px; display: block; } +.ab-seite .ab-marke { font-weight: 800; letter-spacing: 0.08em; font-size: 15px; } +.ab-seite .ab-haupt { flex: 1 0 auto; padding-bottom: 24px; } +.ab-seite .ab-wurzel { + background: var(--ab-karte); border: 1px solid var(--ab-linie); border-radius: 16px; padding: 28px 24px; + overflow-wrap: anywhere; word-break: normal; +} +.ab-seite .ab-laden { margin: 0; color: var(--ab-weich); } +.ab-seite a { color: inherit; text-underline-offset: 2px; } +.ab-seite .ab-vh { + position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; +} +.ab-seite .ab-fuss { flex-shrink: 0; padding: 8px 0 max(24px, env(safe-area-inset-bottom)); font-size: 13px; color: var(--ab-weich); } +.ab-seite .ab-fuss .ab-innen { display: flex; gap: 16px; flex-wrap: wrap; } +.ab-seite .ab-fuss a { color: var(--ab-weich); display: inline-block; min-height: 44px; line-height: 44px; } + +/* ------------------------------------------------------------------ Inhalt --- */ +.ab-seite .ab-pille, .ab-portal .ab-pille { + display: inline-block; margin: 0 0 12px; padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 700; line-height: 1.4; + background: var(--ab-muted-bg); color: var(--ab-muted-text); +} +.ab-seite .ab-pille-warn, .ab-portal .ab-pille-warn { background: var(--ab-warn-bg); color: var(--ab-warn-text); } +.ab-seite .ab-pille-ok, .ab-portal .ab-pille-ok { background: var(--ab-ok-bg); color: var(--ab-ok-text); } +.ab-seite .ab-pille-rot, .ab-portal .ab-pille-rot { background: var(--ab-rot-bg); color: var(--ab-rot-text); } +.ab-seite .ab-h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 12px; } +.ab-seite .ab-h1:focus { outline: none; } +.ab-seite .ab-intro, .ab-seite .ab-text { margin: 0 0 16px; } +.ab-seite .ab-text, .ab-portal .ab-text { white-space: pre-line; } +.ab-seite .ab-kontakt { margin: 0 0 16px; color: var(--ab-weich); } +.ab-seite .ab-h2, .ab-portal .ab-h2 { + font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; color: var(--ab-weich); margin: 24px 0 8px; +} +.ab-seite .ab-liste, .ab-portal .ab-liste { margin: 0 0 8px; padding: 0; } +.ab-seite .ab-zeile, .ab-portal .ab-zeile { + display: grid; grid-template-columns: 9rem 1fr; gap: 4px 12px; padding: 8px 0; border-top: 1px solid var(--ab-linie); +} +.ab-seite .ab-zeile dt, .ab-portal .ab-zeile dt { color: var(--ab-weich); } +.ab-seite .ab-zeile dd, .ab-portal .ab-zeile dd { margin: 0; } +.ab-seite .ab-gesamt, .ab-portal .ab-gesamt { display: flex; justify-content: space-between; gap: 12px; margin: 12px 0 8px; padding-top: 12px; border-top: 2px solid var(--ab-text); } +.ab-seite .ab-gesamt strong, .ab-portal .ab-gesamt strong { font-weight: 800; } +.ab-seite .ab-hinweis, .ab-portal .ab-hinweis { margin: 4px 0 0; font-size: 0.85rem; color: var(--ab-weich); } +.ab-seite .ab-links, .ab-portal .ab-links { list-style: none; margin: 0 0 16px; padding: 0; } +.ab-seite .ab-links a, .ab-portal .ab-links a { display: inline-block; min-height: 44px; line-height: 44px; font-weight: 600; } +.ab-seite .ab-bestaetigt, .ab-portal .ab-bestaetigt { + background: var(--ab-ok-bg); color: var(--ab-ok-text); border-radius: 12px; padding: 12px 16px; margin: 0 0 16px; +} +.ab-seite .ab-bestaetigt p, .ab-portal .ab-bestaetigt p { margin: 2px 0; } +.ab-seite .ab-bestaetigt:focus-visible, .ab-portal .ab-bestaetigt:focus-visible { outline: 3px solid var(--ab-fokus); outline-offset: 2px; } + +/* ------------------------------------------------------------------ Tabelle --- */ +.ab-seite .ab-tabelle, .ab-portal .ab-tabelle { width: 100%; border-collapse: collapse; font-size: 0.95rem; } +.ab-seite .ab-tabelle th, .ab-portal .ab-tabelle th { text-align: left; font-weight: 600; color: var(--ab-weich); padding: 6px 8px 6px 0; border-bottom: 1px solid var(--ab-linie); } +.ab-seite .ab-tabelle td, .ab-portal .ab-tabelle td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--ab-linie); vertical-align: top; } +.ab-seite .ab-tabelle th:nth-child(n+2), .ab-seite .ab-tabelle td:nth-child(n+2), +.ab-portal .ab-tabelle th:nth-child(n+2), .ab-portal .ab-tabelle td:nth-child(n+2) { text-align: right; white-space: nowrap; } +@media (max-width: 640px) { + .ab-seite .ab-tabelle thead, .ab-portal .ab-tabelle thead { + position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; + } + .ab-seite .ab-tabelle, .ab-seite .ab-tabelle tbody, .ab-seite .ab-tabelle tr, .ab-seite .ab-tabelle td, + .ab-portal .ab-tabelle, .ab-portal .ab-tabelle tbody, .ab-portal .ab-tabelle tr, .ab-portal .ab-tabelle td { display: block; } + .ab-seite .ab-tabelle tr, .ab-portal .ab-tabelle tr { padding: 8px 0; border-bottom: 1px solid var(--ab-linie); } + .ab-seite .ab-tabelle td, .ab-portal .ab-tabelle td { + display: flex; justify-content: space-between; gap: 12px; border: 0; padding: 2px 0; text-align: right; white-space: normal; + } + .ab-seite .ab-tabelle td:first-child, .ab-portal .ab-tabelle td:first-child { display: block; text-align: left; font-weight: 700; } + .ab-seite .ab-tabelle td:nth-child(n+2), .ab-portal .ab-tabelle td:nth-child(n+2) { white-space: normal; } + .ab-seite .ab-tabelle td:nth-child(n+2)::before, .ab-portal .ab-tabelle td:nth-child(n+2)::before { content: attr(data-label); color: var(--ab-weich); text-align: left; } + .ab-seite .ab-zeile, .ab-portal .ab-zeile { grid-template-columns: 1fr; gap: 0; } + .ab-seite .ab-wurzel { padding: 22px 18px; } +} + +/* ------------------------------------------------------------------ Formular --- */ +.ab-seite .ab-bestaetigen, .ab-portal .ab-bestaetigen { margin-top: 24px; padding-top: 8px; border-top: 1px solid var(--ab-linie); } +.ab-seite .ab-feldgruppe, .ab-portal .ab-feldgruppe { border: 0; margin: 0; padding: 0; min-width: 0; } +.ab-seite .ab-feldgruppe legend, .ab-portal .ab-feldgruppe legend { font-weight: 700; padding: 0; margin: 12px 0 4px; } +.ab-seite .ab-haken, .ab-portal .ab-haken { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; min-height: 44px; font-size: 15px; cursor: pointer; } +.ab-seite .ab-haken input, .ab-portal .ab-haken input { flex: 0 0 22px; width: 22px; height: 22px; margin: 1px 0 0; accent-color: var(--ab-knopf); } +.ab-seite .ab-frei, .ab-portal .ab-frei { color: var(--ab-weich); font-size: 0.85em; } +.ab-seite .ab-fehler, .ab-portal .ab-fehler { margin: 8px 0; color: var(--ab-fehler); font-weight: 700; } +.ab-seite .ab-fehler[hidden], .ab-portal .ab-fehler[hidden] { display: none; } +.ab-seite .ab-knopf, .ab-portal .ab-knopf { + display: inline-block; min-height: 48px; padding: 0 24px; border: 0; border-radius: 10px; cursor: pointer; + background: var(--ab-knopf); color: var(--ab-knopf-text); font: inherit; font-weight: 700; +} +.ab-seite .ab-knopf:disabled, .ab-portal .ab-knopf:disabled { opacity: 0.45; cursor: default; } +.ab-seite .ab-knopf:focus-visible, .ab-portal .ab-knopf:focus-visible, +.ab-seite .ab-link:focus-visible, .ab-portal .ab-link:focus-visible { outline: 3px solid var(--ab-fokus); outline-offset: 2px; } +.ab-seite .ab-notiz, .ab-portal .ab-notiz, .ab-seite .ab-gesperrt, .ab-portal .ab-gesperrt { margin: 12px 0 0; font-size: 0.85rem; color: var(--ab-weich); } +@media (max-width: 640px) { + .ab-seite .ab-knopf, .ab-portal .ab-knopf { width: 100%; } +} + +/* ------------------------------------------------------------------ Installierte App --- */ +.ab-app-back { display: none; } +@media (display-mode: standalone) { + .ab-app-back { + display: inline-block; margin: calc(env(safe-area-inset-top) + 12px) 16px 0; font-size: 15px; font-weight: 600; color: var(--ab-text); text-decoration: none; + } + .ab-seite .ab-kopf { padding-top: 12px; } +} diff --git a/public/auftrag.html b/public/auftrag.html new file mode 100644 index 0000000..7cbf4a4 --- /dev/null +++ b/public/auftrag.html @@ -0,0 +1,42 @@ + + + + + + + + + + + Auftragsbestätigung – WEBklar + + + + + + + + ‹ Zurück zum Kundenbereich +
+
WEBklar
+
+
+
+
+ +

Ihre Auftragsbestätigung wird geladen …

+
+
+
+
+ + + + diff --git a/test/auftraegeKundenportalHuelle.test.js b/test/auftraegeKundenportalHuelle.test.js new file mode 100644 index 0000000..7649c53 --- /dev/null +++ b/test/auftraegeKundenportalHuelle.test.js @@ -0,0 +1,52 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +const lies = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8') +const html = lies('public/auftrag.html') +const css = lies('public/auftrag.css') + +test('auftrag.html: noindex, kein Referrer, Viewport mit Notch, Titel, ein Modul-Skript, kein Inline-Skript', () => { + assert.match(html, //) + assert.match(html, //) + assert.match(html, //) + assert.match(html, //) + assert.match(html, /Auftragsbestätigung – WEBklar<\/title>/) + assert.match(html, /<link rel="stylesheet" href="\/auftrag\.css">/) + assert.deepEqual(html.match(/<script\b[^>]*>/g), ['<script type="module" src="/auftrag.js">']) + assert.doesNotMatch(html, /<script>/) + assert.doesNotMatch(html, /\son[a-z]+=/i, 'keine Ereignis-Attribute') + assert.doesNotMatch(html, /https?:\/\/(?!webklar\.com\/impressum)/, 'keine Fremdressourcen') +}) + +test('auftrag.html: Wurzel im Zustand „lädt“, Live-Region, Hinweis ohne JavaScript', () => { + assert.match(html, /<div class="ab-wurzel" id="ab-wurzel" data-zustand="laedt" aria-busy="true">/) + assert.match(html, /<p class="ab-laden">Ihre Auftragsbestätigung wird geladen …<\/p>/) + assert.match(html, /<div class="ab-vh" id="ab-ansage" aria-live="polite"><\/div>/) + assert.match(html, /<noscript><p>Bitte aktivieren Sie JavaScript, um Ihren Auftrag zu bestätigen\.<\/p><\/noscript>/) + assert.match(html, /<noscript><style>\.ab-laden\{display:none\}<\/style><\/noscript>/) +}) + +test('auftrag.html: Fußzeile mit Datenschutz und Impressum; Zurück-Link nur für die installierte App', () => { + assert.match(html, /<footer class="ab-fuss">[\s\S]*<a href="\/datenschutz\.html" target="_blank" rel="noopener noreferrer">Datenschutz<\/a>[\s\S]*<a href="https:\/\/webklar\.com\/impressum" target="_blank" rel="noopener noreferrer">Impressum<\/a>[\s\S]*<\/footer>/) + assert.match(html, /<a class="ab-app-back" href="\/dashboard\.html">‹ Zurück zum Kundenbereich<\/a>/) +}) + +test('auftrag.css: Farben der Mailvorlagen hell und dunkel, Standalone-Modus, Handy-Tabelle, Umbruch, Tippflächen', () => { + for (const farbe of ['#f1f0ec', '#efe6fb', '#5a2a91', '#e4f2e6', '#1d5b2c', '#161615', '#1f1f1e', '#f2f1ee', '#a8a6a0']) assert.ok(css.includes(farbe), farbe) + assert.match(css, /@media \(prefers-color-scheme: dark\)/) + assert.match(css, /@media \(display-mode: standalone\)/) + assert.match(css, /env\(safe-area-inset-top\)/) + assert.match(css, /overflow-wrap:\s*anywhere/) + assert.match(css, /min-height:\s*48px/) + assert.match(css, /content:\s*attr\(data-label\)/) + assert.match(css, /\.ab-app-back\s*\{[^}]*display:\s*none/) + assert.match(css, /\.ab-vh\s*\{/) + assert.match(css, /body\.ab-seite[^{]*\.hidden|\.ab-seite \.hidden/) +}) + +test('Service Worker und Manifest brauchen für die neue Seite keinen Eintrag', () => { + assert.doesNotMatch(lies('public/sw.js'), /auftrag/) + const manifest = JSON.parse(lies('public/manifest.webmanifest')) + assert.equal(manifest.scope, '/', 'der Link aus der Mail kann in der installierten App öffnen: darum der Standalone-Modus oben') +})