module.css: Gerüst, Prüftest und gemeinsame Bausteine unter .kn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5534655671
commit
a6a656f18a
2 files changed
+234
-2
No files matched your search
+85
-2
@@ -1,2 +1,85 @@
|
||||
/* Kundenansicht neu: Stile der gemeinsamen Funktionsmodule (Terminplaner, Webshop, Buchungskalender, Öffnungszeiten, News, Analyse) unter .kn.
|
||||
Besitzer: P4. Hier steht bis dahin nichts. */
|
||||
/* Kundenansicht neu: Stile der gemeinsamen Funktionsmodule (Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, News, Analyse) unter .kn.
|
||||
Besitzer: P4. Die Module sind dieselben Dateien wie in der alten Ansicht und bringen ihre Klassen mit (tm-, sh-, bk-, zt-, fn-, nw-, an-, hs-);
|
||||
diese Datei ersetzt termine.css, shop.css, buchungen.css, zeiten.css, funktionen.css (News) und analyse.css vollständig. Jede Regel steht unter .kn.
|
||||
Aussehen wie das Ticketsystem: Abschnitte mit Haarlinien statt Karten, Pillen für Zustände, Ring-Schatten, ein Akzent (#111). Der Prüftest
|
||||
test/kundenansichtNeuModulCss.test.js sichert Scope, Palette, Verbote und die Abdeckung aller Klassen der alten Dateien.
|
||||
Abschnitte: 0 gemeinsame Bausteine, 1 Terminplaner, 2 Webshop und Buchungskalender, 3 Öffnungszeiten live, 4 News, 5 Analyse. */
|
||||
|
||||
/* ============================================================ 0 Gemeinsame Bausteine === */
|
||||
|
||||
/* Abschnitt statt Karte: ohne Fläche, Rahmen und Radius; ab dem zweiten Abschnitt eine Haarlinie darüber */
|
||||
.kn .kb-panel { max-width: none; margin: 0; padding: 20px 0; }
|
||||
.kn .kb-panel + .kb-panel { border-top: 1px solid var(--kn-line); }
|
||||
.kn .kb-panel h3 { margin: 0 0 6px; font-size: 15px; line-height: 22px; font-weight: 600; }
|
||||
.kn .kb-panel h4 { margin: 16px 0 4px; font-size: 14px; line-height: 20px; font-weight: 600; }
|
||||
.kn .kb-panel p { margin: 0 0 10px; }
|
||||
.kn .kb-panel p:last-child { margin-bottom: 0; }
|
||||
.kn .kb-empty { margin: 0; padding: 24px 0; color: var(--kn-text-2); }
|
||||
|
||||
/* Link im Text (auch als Knopf mit data-view) */
|
||||
.kn .kb-link { display: inline; padding: 0; border: 0; background: none; font: inherit; color: var(--kn-text); text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; cursor: pointer; }
|
||||
.kn .kb-link:hover { text-decoration-color: currentColor; }
|
||||
|
||||
/* Aufzählung mit Häkchen */
|
||||
.kn .kb-checks { margin: 12px 0; padding: 0; list-style: none; }
|
||||
.kn .kb-checks li { position: relative; padding: 4px 0 4px 24px; }
|
||||
.kn .kb-checks li::before { content: '✓'; position: absolute; left: 0; color: var(--kn-gruen-fg); font-weight: 600; }
|
||||
|
||||
.kn .abo-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 16px; }
|
||||
.kn .muted { color: var(--kn-text-2); }
|
||||
.kn .error { margin: 0 0 12px; font-size: 13px; color: var(--kn-danger); }
|
||||
|
||||
/* Knöpfe: Hauptaktion schwarz, sonst weiß mit Ring-Schatten */
|
||||
.kn .kb-btn-primary, .kn .abo-btn, .kn .kb-btn, .kn .kb-btn-outline {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; box-sizing: border-box; min-height: 36px; padding: 0 14px;
|
||||
border: 0; border-radius: var(--kn-radius); font: inherit; font-weight: 500; line-height: 20px; text-align: center; text-decoration: none; cursor: pointer;
|
||||
transition: background-color .12s, box-shadow .12s;
|
||||
}
|
||||
.kn .kb-btn-primary, .kn .abo-btn { background: var(--kn-accent); color: #fff; box-shadow: var(--kn-shadow-btn); }
|
||||
.kn .kb-btn-primary:hover, .kn .abo-btn:hover { background: var(--kn-accent-hover); }
|
||||
.kn .kb-btn, .kn .kb-btn-outline { background: var(--kn-bg); color: var(--kn-text); box-shadow: var(--kn-ring); }
|
||||
.kn .kb-btn:hover, .kn .kb-btn-outline:hover { background: var(--kn-bg-hover); }
|
||||
.kn .kb-btn-primary:disabled, .kn .abo-btn:disabled, .kn .kb-btn:disabled, .kn .kb-btn-outline:disabled { opacity: .4; cursor: not-allowed; }
|
||||
|
||||
/* Felder der Module: weiß mit Ring-Schatten, 6 px Radius, 36 px hoch */
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) :is(input, select, textarea):not([type=checkbox]):not([type=radio]):not([type=color]):not([type=file]),
|
||||
.kn .tm-link-feld, .kn .tm-code-feld, .kn .zt-google-text, .kn .bk-link input, .kn input[readonly] {
|
||||
box-sizing: border-box; min-width: 0; min-height: 36px; padding: 6px 10px; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); color: var(--kn-text);
|
||||
box-shadow: var(--kn-ring); font: inherit; line-height: 20px; outline: none; transition: box-shadow .12s;
|
||||
}
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) textarea,
|
||||
.kn .tm-code-feld, .kn .zt-google-text { min-height: 80px; resize: vertical; }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) select,
|
||||
.kn .zt-feiertag select {
|
||||
padding-right: 28px; appearance: none; -webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235e6678' stroke-width='1.5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat; background-position: right 10px center;
|
||||
}
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) :is(input, select, textarea):focus,
|
||||
.kn .tm-link-feld:focus, .kn .tm-code-feld:focus, .kn .zt-google-text:focus { box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit) [aria-invalid='true'] { box-shadow: 0 0 0 1px var(--kn-danger), 0 0 0 4px var(--kn-danger-ring); }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit) input::placeholder { color: var(--kn-text-3); }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field) input[type=color] { min-height: 36px; padding: 2px; cursor: pointer; }
|
||||
.kn input[type=checkbox], .kn input[type=radio] { accent-color: var(--kn-accent); }
|
||||
|
||||
/* Fokus schwarz, überall gleich */
|
||||
.kn button:focus-visible, .kn input:focus-visible, .kn select:focus-visible, .kn textarea:focus-visible, .kn summary:focus-visible, .kn a:focus-visible, .kn [tabindex]:focus-visible {
|
||||
outline: 2px solid #111; outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Screenreader-Text */
|
||||
.kn .tm-sr, .kn .sh-sr, .kn .an-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
/* Handy: Tippflächen 44 px, Felder 16 px gegen den Zoom von iOS */
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn .kb-btn-primary, .kn .abo-btn, .kn .kb-btn, .kn .kb-btn-outline { min-height: 44px; padding: 0 16px; }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) :is(input, select, textarea):not([type=checkbox]):not([type=radio]):not([type=color]):not([type=file]),
|
||||
.kn .tm-link-feld, .kn .tm-code-feld, .kn .zt-google-text, .kn .bk-link input, .kn input[readonly] { min-height: 44px; font-size: 16px; }
|
||||
.kn .kb-link { display: inline-flex; align-items: center; min-height: 44px; }
|
||||
}
|
||||
|
||||
/* Reduzierte Bewegung: keine Übergänge */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kn .kb-btn-primary, .kn .abo-btn, .kn .kb-btn, .kn .kb-btn-outline { transition: none; }
|
||||
.kn :is(.tm-feld, .sh-feld, .nw-field, .zt-tag, .zt-zeit, .zt-regler, .tm-sprung, .fn-project, .bk-posten, .bk-staffel, .bk-filter, .bk-vorschaufelder, .bk-importform, .bk-personpreis, .an-heat-bar) :is(input, select, textarea) { transition: none; }
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
|
||||
// Prüftest für public/neu/module.css (P4 Tasks 9 bis 13): jede Regel steht unter .kn, nur Farben der Palette, keine Verläufe, kein Glas,
|
||||
// kein Dunkelmodus, keine Animation; für jedes Präfix aus FERTIG ist jede Klasse der alten CSS-Datei unter .kn gestaltet.
|
||||
|
||||
const PUBLIC = new URL('../public/', import.meta.url)
|
||||
const lies = (datei) => readFileSync(new URL(datei, PUBLIC), 'utf8')
|
||||
const neuDateien = () => readdirSync(new URL('neu/', PUBLIC)).filter((f) => f.endsWith('.css')).sort()
|
||||
const neuCss = () => neuDateien().map((f) => lies(`neu/${f}`)).join('\n')
|
||||
const MODUL = () => lies('neu/module.css')
|
||||
const jsModule = () => [
|
||||
...readdirSync(PUBLIC).filter((f) => f.endsWith('.js')).map((f) => lies(f)),
|
||||
...readdirSync(new URL('neu/', PUBLIC)).filter((f) => f.endsWith('.js')).map((f) => lies(`neu/${f}`)),
|
||||
].join('\n')
|
||||
|
||||
const ohneKommentare = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
|
||||
/** [{ selektor, koerper }] – verschachtelte @media/@supports flach, @keyframes und @font-face ohne Schritte. */
|
||||
function regeln(css) {
|
||||
const text = ohneKommentare(css)
|
||||
const lies2 = (quelle) => {
|
||||
const aus = []
|
||||
let i = 0
|
||||
while (i < quelle.length) {
|
||||
const auf = quelle.indexOf('{', i)
|
||||
if (auf === -1) break
|
||||
const kopf = quelle.slice(i, auf).trim()
|
||||
let tiefe = 1
|
||||
let j = auf + 1
|
||||
while (j < quelle.length && tiefe > 0) {
|
||||
if (quelle[j] === '{') tiefe += 1
|
||||
else if (quelle[j] === '}') tiefe -= 1
|
||||
j += 1
|
||||
}
|
||||
const innen = quelle.slice(auf + 1, j - 1)
|
||||
if (/^@(media|supports|container|layer)\b/.test(kopf)) aus.push(...lies2(innen))
|
||||
else if (!kopf.startsWith('@')) aus.push({ selektor: kopf, koerper: innen })
|
||||
i = j
|
||||
}
|
||||
return aus
|
||||
}
|
||||
return lies2(text)
|
||||
}
|
||||
|
||||
/** Selektoren einer Regelliste; Kommalisten aufgespalten, Klammer-bewusst (:is(), :not(), [attr="a,b"]). */
|
||||
function selektoren(css) {
|
||||
const aus = []
|
||||
for (const { selektor } of regeln(css)) {
|
||||
let tiefe = 0
|
||||
let quelle = ''
|
||||
let anfuehrung = null
|
||||
for (const z of selektor) {
|
||||
if (anfuehrung) { quelle += z; if (z === anfuehrung) anfuehrung = null; continue }
|
||||
if (z === '"' || z === "'") { anfuehrung = z; quelle += z; continue }
|
||||
if (z === '(' || z === '[') tiefe += 1
|
||||
if (z === ')' || z === ']') tiefe -= 1
|
||||
if (z === ',' && tiefe === 0) { aus.push(quelle.trim()); quelle = ''; continue }
|
||||
quelle += z
|
||||
}
|
||||
if (quelle.trim()) aus.push(quelle.trim())
|
||||
}
|
||||
return aus
|
||||
}
|
||||
|
||||
/** Klassen mit Präfix aus den Selektoren (nur echte Klassen: ein Punkt direkt davor, nie `.kn-fn-…` für `fn`). */
|
||||
function klassen(css, praefix) {
|
||||
const treffer = new Set()
|
||||
const muster = new RegExp(`\\.(${praefix}-[A-Za-z0-9_-]+)`, 'g')
|
||||
for (const s of selektoren(css)) for (const m of s.matchAll(muster)) treffer.add(m[1])
|
||||
return treffer
|
||||
}
|
||||
|
||||
const FERTIG = [] // Präfixe mit geprüfter Abdeckung; Task 10 bis 13 ergänzen sie
|
||||
const UNGENUTZT = new Set([]) // alte Klassen, die in keiner JS-Datei vorkommen; der Test prüft das
|
||||
const ALT = { tm: 'termine.css', sh: 'shop.css', bk: 'buchungen.css', zt: 'zeiten.css', an: 'analyse.css', hs: 'analyse.css', nw: 'funktionen.css' }
|
||||
const FN_NEWS = ['fn-project', 'fn-booking', 'fn-price', 'fn-estimate', 'fn-desc-link', 'fn-consent', 'fn-thanks', 'fn-status', 'fn-facts', 'fn-pill', 'fn-pill-setup']
|
||||
const ALT_KLASSEN = ['kb-panel', 'kb-btn', 'kb-btn-primary', 'kb-btn-outline', 'kb-empty', 'kb-link', 'kb-checks', 'abo-actions', 'abo-btn', 'hidden', 'muted', 'error']
|
||||
const PALETTE = new Set(['#fff', '#ffffff', '#1a1f36', '#5e6678', '#8a93a3', '#ebeef1', '#d8dde3', '#f6f8fa', '#f8f9fb', '#111', '#111111', '#414552', '#e0f2fe', '#075985', '#fcedb9', '#a82c00', '#efeafe', '#5b21b6', '#d7f7c2', '#006908', '#ffe7f2', '#b3093c', '#c0123c'])
|
||||
const ZETTEL = /#fff6d6|#ddeffb|#ffe1d9/i
|
||||
const RING = new Set(['60,66,87,.16', '60,66,87,0.16', '0,0,0,.06', '0,0,0,0.06'])
|
||||
const HANDY = '@media (max-width: 880px), (pointer: coarse) and (max-height: 500px)'
|
||||
|
||||
test('Hilfen: Regeln, Selektoren und Klassen werden richtig gelesen', () => {
|
||||
const css = '/* x */ .a, .b:not(.c, .d) { color: #fff } @media (x) { .e { margin: 0 } } @keyframes k { from { opacity: 0 } } .f[data-x="a,b"] { top: 0 }'
|
||||
assert.deepEqual(regeln(css).map((r) => r.selektor), ['.a, .b:not(.c, .d)', '.e', '.f[data-x="a,b"]'])
|
||||
assert.deepEqual(selektoren(css), ['.a', '.b:not(.c, .d)', '.e', '.f[data-x="a,b"]'])
|
||||
assert.deepEqual([...klassen('.kn .tm-a, .kn .kn-tm-b, .tm-c-d:hover { }', 'tm')].sort(), ['tm-a', 'tm-c-d'])
|
||||
assert.deepEqual([...klassen('.kn-fn-x, .fn-y { }', 'fn')], ['fn-y'])
|
||||
})
|
||||
|
||||
test('module.css: jede Regel steht unter .kn und hängt nicht an der alten Seite', () => {
|
||||
const alle = selektoren(MODUL())
|
||||
assert.ok(alle.length > 0, 'die Datei hat Regeln')
|
||||
for (const s of alle) assert.match(s, /^\.kn(\s|[.:#[>~+]|$)/, s)
|
||||
assert.doesNotMatch(MODUL(), /#view-|--kb-|\.kb-page/)
|
||||
})
|
||||
|
||||
test('module.css: Verbote (Lila, Teal, Verlauf, Glas, Dunkel, Gewicht 800, Animation)', () => {
|
||||
assert.doesNotMatch(MODUL(), /#9d3dfe|#d4b4ff|157,\s*61,\s*254|#2dd4bf|#5eead4|gradient|backdrop-filter|color-scheme:\s*dark|prefers-color-scheme|font-weight:\s*(800|900)|@keyframes/)
|
||||
})
|
||||
|
||||
test('module.css: Farben nur aus der Palette der Übersicht; Zettelfarben nur in .nw-paper-*; rgba nur der Ring-Schatten', () => {
|
||||
for (const { selektor, koerper } of regeln(MODUL())) {
|
||||
for (const [, hex] of koerper.matchAll(/(#[0-9a-fA-F]{3,8})\b/g)) assert.ok(PALETTE.has(hex.toLowerCase()) || (ZETTEL.test(hex) && /\.nw-paper-/.test(selektor)), `${selektor}: ${hex}`)
|
||||
for (const [, wert] of koerper.matchAll(/rgba?\(([^)]*)\)/g)) assert.ok(RING.has(wert.replace(/\s/g, '')), `${selektor}: rgba(${wert})`)
|
||||
}
|
||||
})
|
||||
|
||||
test('module.css: Fokus schwarz, Handy-Regeln wörtlich mit 44px, reduzierte Bewegung', () => {
|
||||
const css = MODUL()
|
||||
assert.match(css, /:focus-visible[^{]*\{[^}]*outline:\s*2px solid (#111|var\(--kn-[a-z-]+\))/)
|
||||
assert.ok(css.includes(HANDY) && /min-height:\s*44px/.test(css.slice(css.indexOf(HANDY))))
|
||||
assert.match(css, /prefers-reduced-motion:\s*reduce/)
|
||||
})
|
||||
|
||||
test('module.css: Animationen höchstens als transition bis 150 ms', () => {
|
||||
for (const { selektor, koerper } of regeln(MODUL())) {
|
||||
assert.doesNotMatch(koerper, /\banimation(-name)?\s*:/, selektor)
|
||||
for (const [, wert] of koerper.matchAll(/transition(?:-duration)?:\s*([^;]*)/g)) {
|
||||
for (const [, zahl, einheit] of wert.matchAll(/(\d*\.?\d+)(ms|s)\b/g)) assert.ok((einheit === 's' ? Number(zahl) * 1000 : Number(zahl)) <= 150, `${selektor}: ${wert}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('module.css: jedes var(--kn-…) ist in public/neu/*.css definiert', () => {
|
||||
const definiert = new Set([...neuCss().matchAll(/(--kn-[a-z0-9-]+)\s*:/g)].map((m) => m[1]))
|
||||
for (const [, name] of MODUL().matchAll(/var\((--kn-[a-z0-9-]+)/g)) assert.ok(definiert.has(name), name)
|
||||
})
|
||||
|
||||
test('Alt-Klassen der Module sind unter .kn gestaltet (in einer Datei von public/neu)', () => {
|
||||
const sel = selektoren(neuCss()).filter((s) => s.startsWith('.kn'))
|
||||
for (const k of ALT_KLASSEN) assert.ok(sel.some((s) => new RegExp(`\\.${k}(?![a-z0-9-])`).test(s)), k)
|
||||
})
|
||||
|
||||
test('UNGENUTZT enthält nur Klassen, die kein JS nennt', () => {
|
||||
const js = jsModule()
|
||||
for (const k of UNGENUTZT) assert.ok(!js.includes(k), k)
|
||||
})
|
||||
|
||||
for (const praefix of FERTIG) {
|
||||
test(`Abdeckung ${praefix}: jede Klasse der alten CSS-Datei ist unter .kn gestaltet`, () => {
|
||||
const alt = praefix === 'fn' ? FN_NEWS : [...klassen(lies(ALT[praefix]), praefix)]
|
||||
const neu = klassen(MODUL(), praefix)
|
||||
assert.ok(alt.length > 0, 'es gibt alte Klassen')
|
||||
assert.deepEqual(alt.filter((k) => !neu.has(k) && !UNGENUTZT.has(k)).sort(), [])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user