Kundenportal Aufträge: auftrag.html und auftrag.css (noindex, Mail-Stil, Fußzeile, Standalone)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
38734e99aa
commit
cad9ba7e40
3 files changed
+226
No files matched your search
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<meta name="theme-color" content="#f1f0ec" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#161615" media="(prefers-color-scheme: dark)">
|
||||||
|
<title>Auftragsbestätigung – WEBklar</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="stylesheet" href="/auftrag.css">
|
||||||
|
<noscript><style>.ab-laden{display:none}</style></noscript>
|
||||||
|
</head>
|
||||||
|
<body class="ab-seite">
|
||||||
|
<!-- Bestätigungsseite für eine Auftragsbestätigung (Link aus der Mail K31: /auftrag.html#t=<Token>, Rückfall ?t=<Token>).
|
||||||
|
Der Token steht nur in der Adresse und im Speicher der Seite: keine Cookies, kein Browserspeicher, keine Fremdanfragen,
|
||||||
|
nichts im Protokoll (auftrag.js). Ohne Anmeldung erreichbar, nicht für Suchmaschinen. -->
|
||||||
|
<a class="ab-app-back" href="/dashboard.html">‹ Zurück zum Kundenbereich</a>
|
||||||
|
<header class="ab-kopf">
|
||||||
|
<div class="ab-innen"><img class="ab-logo" src="/webklar-logo.png" width="36" height="36" alt=""><span class="ab-marke">WEBklar</span></div>
|
||||||
|
</header>
|
||||||
|
<main class="ab-haupt">
|
||||||
|
<div class="ab-innen">
|
||||||
|
<div class="ab-wurzel" id="ab-wurzel" data-zustand="laedt" aria-busy="true">
|
||||||
|
<noscript><p>Bitte aktivieren Sie JavaScript, um Ihren Auftrag zu bestätigen.</p></noscript>
|
||||||
|
<p class="ab-laden">Ihre Auftragsbestätigung wird geladen …</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div class="ab-vh" id="ab-ansage" aria-live="polite"></div>
|
||||||
|
<footer class="ab-fuss">
|
||||||
|
<div class="ab-innen">
|
||||||
|
<a href="/datenschutz.html" target="_blank" rel="noopener noreferrer">Datenschutz</a>
|
||||||
|
<a href="https://webklar.com/impressum" target="_blank" rel="noopener noreferrer">Impressum</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="/auftrag.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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, /<meta name="robots" content="noindex, nofollow">/)
|
||||||
|
assert.match(html, /<meta name="referrer" content="no-referrer">/)
|
||||||
|
assert.match(html, /<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">/)
|
||||||
|
assert.match(html, /<meta name="color-scheme" content="light dark">/)
|
||||||
|
assert.match(html, /<title>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')
|
||||||
|
})
|
||||||
Reference in new issue
Block a user