Funktionen neu: Aussehen (weiß, Pillen, 44-px-Zeilen) mit Quelltext-Prüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
36890c6734
commit
9ca7900213
2 files changed
+179
-2
No files matched your search
@@ -1,2 +1,86 @@
|
||||
/* Kundenansicht neu: Stile der Ansicht Funktionen (Meine, Entdecken) und der Einschalt-Ansicht.
|
||||
Besitzer: P3. Hier steht bis dahin nichts. */
|
||||
/* Kundenansicht neu (P3): Aussehen der Ansicht Funktionen (Meine, Entdecken) und der Bestätigungsseite zum Einschalten.
|
||||
Besitzer: P3. Weißer Stil: Haarlinien statt Karten, Radien 6 und 12 px (Pillen 999 px), kein Schatten außer dem Ring, kein Gewicht 800,
|
||||
keine Animation. Farben als var(--kn-…, Wert) mit den Werten der Übersicht als Fallback. Tap-Ziele sind 44 px hoch. */
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Pillen --- */
|
||||
.kn-fn-pille {
|
||||
display: inline-flex; align-items: center; flex: none; height: 20px; padding: 0 8px; border-radius: 999px;
|
||||
font-size: 12px; line-height: 20px; font-weight: 500; white-space: nowrap; font-variant-numeric: tabular-nums;
|
||||
background: var(--kn-grau-bg, #ebeef1); color: var(--kn-grau-fg, #414552);
|
||||
}
|
||||
.kn-fn-pille--grau { background: var(--kn-grau-bg, #ebeef1); color: var(--kn-grau-fg, #414552); }
|
||||
.kn-fn-pille--blau { background: var(--kn-blau-bg, #e0f2fe); color: var(--kn-blau-fg, #075985); }
|
||||
.kn-fn-pille--orange { background: var(--kn-orange-bg, #fcedb9); color: var(--kn-orange-fg, #a82c00); }
|
||||
.kn-fn-pille--violett { background: var(--kn-violett-bg, #efeafe); color: var(--kn-violett-fg, #5b21b6); }
|
||||
.kn-fn-pille--gruen { background: var(--kn-gruen-bg, #d7f7c2); color: var(--kn-gruen-fg, #006908); }
|
||||
.kn-fn-pille--rot { background: var(--kn-rot-bg, #ffe7f2); color: var(--kn-rot-fg, #b3093c); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Knöpfe --- */
|
||||
.kn-fn-knopf {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; padding: 0 16px;
|
||||
border: 0; border-radius: 6px; font-weight: 500; text-align: center; text-decoration: none; cursor: pointer;
|
||||
background: var(--kn-accent, #111); color: #fff;
|
||||
}
|
||||
.kn-fn-knopf:hover { background: var(--kn-accent-hover, #2b2b2b); }
|
||||
.kn-fn-knopf:disabled, .kn-fn-knopf:disabled:hover {
|
||||
background: var(--kn-bg-subtle, #f8f9fb); color: var(--kn-text-3, #8a93a3); cursor: not-allowed; opacity: 1;
|
||||
box-shadow: var(--kn-ring, 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06));
|
||||
}
|
||||
.kn-fn-knopf--zweit {
|
||||
background: var(--kn-bg, #fff); color: var(--kn-text, #1a1f36);
|
||||
box-shadow: var(--kn-ring, 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06));
|
||||
}
|
||||
.kn-fn-knopf--zweit:hover { background: var(--kn-bg-hover, #f6f8fa); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Meldungen --- */
|
||||
.kn-fn-meldung { margin-top: 8px; font-size: 13px; line-height: 20px; color: var(--kn-danger, #c0123c); overflow-wrap: anywhere; }
|
||||
.kn-fn-meldung:empty { display: none; }
|
||||
.kn-fn-hinweis, .kn-fn-info { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; }
|
||||
.kn-fn-info:first-child { margin-top: 0; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Meine --- */
|
||||
.kn-fn-liste { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-fn-zeile { border-bottom: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-zeile:first-child { border-top: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-zeile-knopf {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; min-height: 44px; padding: 12px 0;
|
||||
text-align: left; background: none; border: 0; cursor: pointer; color: var(--kn-text, #1a1f36);
|
||||
}
|
||||
.kn-fn-zeile-knopf:hover .kn-fn-name { color: var(--kn-accent, #111); text-decoration: underline; text-decoration-color: var(--kn-line-strong, #d8dde3); text-underline-offset: 3px; }
|
||||
.kn-fn-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.kn-fn-name { font-weight: 600; color: var(--kn-text, #1a1f36); overflow-wrap: anywhere; }
|
||||
.kn-fn-zeile2 { font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Entdecken --- */
|
||||
.kn-fn-eintrag { display: flex; flex-direction: column; gap: 16px; padding: 24px 0; border-top: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-eintrag:last-child { border-bottom: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-bild { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 12px; background: var(--kn-bg-subtle, #f8f9fb); }
|
||||
.kn-fn-eintrag-text { min-width: 0; }
|
||||
.kn-fn-eintrag-text h3 { font-size: 16px; line-height: 24px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-fn-beschreibung { margin-top: 4px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; }
|
||||
.kn-fn-enthalten { margin: 8px 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-wahl { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Bestätigungsseite --- */
|
||||
.kn-fn-einschalten { max-width: 560px; }
|
||||
.kn-fn-einschalten h2 { margin-bottom: 16px; font-size: 20px; line-height: 28px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-fn-fakten { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 0 0 16px; }
|
||||
.kn-fn-fakten > div { display: contents; }
|
||||
.kn-fn-fakten dt { color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-fakten dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-fn-konto, .kn-fn-recht { margin: 0 0 12px; overflow-wrap: anywhere; }
|
||||
.kn-fn-konto { color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-recht a { text-decoration: underline; text-decoration-color: var(--kn-line-strong, #d8dde3); text-underline-offset: 3px; }
|
||||
.kn-fn-recht a:hover { text-decoration-color: currentColor; }
|
||||
.kn-fn-einschalten .kn-fn-hinweis { margin: 0 0 12px; }
|
||||
.kn-fn-haken { display: flex; align-items: flex-start; gap: 12px; min-height: 44px; margin: 12px 0 0; cursor: pointer; }
|
||||
.kn-fn-haken input { flex: none; width: 20px; height: 20px; margin: 0; accent-color: var(--kn-accent, #111); }
|
||||
.kn-fn-haken span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-fn-aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Breite Fenster --- */
|
||||
@media (min-width: 881px) {
|
||||
.kn-fn-eintrag { flex-direction: row; align-items: flex-start; gap: 24px; }
|
||||
.kn-fn-bild { flex: 0 0 320px; width: 320px; }
|
||||
.kn-fn-eintrag-text { flex: 1 1 0; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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 ohneKommentare = (t) => t.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
||||
const JS = ['public/neu/ansicht-funktionen.js', 'public/neu/funktion-einschalten.js']
|
||||
const css = lies('public/neu/funktionen.css')
|
||||
|
||||
test('funktionen.css: Pillen-Farben aus den Global Constraints', () => {
|
||||
const PILLEN = { grau: ['#ebeef1', '#414552'], blau: ['#e0f2fe', '#075985'], orange: ['#fcedb9', '#a82c00'], violett: ['#efeafe', '#5b21b6'], gruen: ['#d7f7c2', '#006908'], rot: ['#ffe7f2', '#b3093c'] }
|
||||
for (const [ton, [flaeche, schrift]] of Object.entries(PILLEN)) {
|
||||
const regel = (css.match(new RegExp(`\\.kn-fn-pille--${ton}\\s*\\{([^}]*)\\}`)) ?? [, ''])[1].toLowerCase()
|
||||
assert.ok(regel.includes(flaeche) && regel.includes(schrift), ton)
|
||||
}
|
||||
})
|
||||
|
||||
test('funktionen.css: verbotene Mittel fehlen, Radien nur 6 und 12 px, Tap-Ziel 44 px', () => {
|
||||
assert.doesNotMatch(css, /gradient|backdrop-filter|glow|prefers-color-scheme\s*:\s*dark|#9d3dfe|rgba\(\s*157/i)
|
||||
assert.doesNotMatch(css, /font-weight\s*:\s*(800|900)/)
|
||||
for (const m of css.matchAll(/border-radius\s*:\s*([^;}]+)/g)) assert.match(m[1].trim(), /^(0|6px|12px|999px|50%)$/, m[0])
|
||||
assert.match((css.match(/\.kn-fn-zeile-knopf\s*\{([^}]*)\}/) ?? [, ''])[1], /min-height\s*:\s*44px/)
|
||||
assert.match(css, /@media\s*\(min-width:\s*881px\)/)
|
||||
})
|
||||
|
||||
test('funktionen.css: Knöpfe 44 px hoch, Primärknopf #111 mit weißer Schrift, gesperrt grau; keine Schatten außer dem Ring; keine Animation', () => {
|
||||
const regel = (selektor) => (css.match(new RegExp(`${selektor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`)) ?? [, ''])[1]
|
||||
const knopf = regel('.kn-fn-knopf')
|
||||
assert.match(knopf, /min-height\s*:\s*44px/)
|
||||
assert.match(knopf, /#111/i)
|
||||
assert.match(knopf, /#fff/i)
|
||||
const gesperrt = (css.match(/\.kn-fn-knopf:disabled[^{]*\{([^}]*)\}/) ?? [, ''])[1]
|
||||
assert.match(gesperrt, /#8a93a3/i)
|
||||
assert.match(gesperrt, /#f8f9fb/i)
|
||||
assert.match(regel('.kn-fn-knopf--zweit'), /0 0 0 1px rgba\(60,\s*66,\s*87,\s*\.16\)/)
|
||||
assert.match(regel('.kn-fn-meldung'), /#c0123c/i)
|
||||
for (const m of css.matchAll(/box-shadow\s*:\s*([^;}]+)/g)) assert.match(m[1], /^(none|var\(--kn-ring,\s*0 0 0 1px rgba\(60,\s*66,\s*87,\s*\.16\),\s*0 1px 1px rgba\(0,\s*0,\s*0,\s*\.06\)\))$/, m[0])
|
||||
assert.doesNotMatch(css, /@keyframes|animation\s*:|transition\s*:/)
|
||||
})
|
||||
|
||||
test('funktionen.css: das Bild ist 16:10 mit Radius 12 px, Entdecken hat keinen Rahmen', () => {
|
||||
const bild = (css.match(/\.kn-fn-bild\s*\{([^}]*)\}/) ?? [, ''])[1]
|
||||
assert.match(bild, /aspect-ratio\s*:\s*16\s*\/\s*10/)
|
||||
assert.match(bild, /border-radius\s*:\s*12px/)
|
||||
const eintrag = (css.match(/\.kn-fn-eintrag\s*\{([^}]*)\}/) ?? [, ''])[1]
|
||||
assert.doesNotMatch(eintrag, /\bborder\s*:|box-shadow|border-radius/)
|
||||
})
|
||||
|
||||
test('jede kn-fn-Klasse der JS-Dateien ist im CSS gestaltet', () => {
|
||||
for (const datei of JS) for (const k of new Set(ohneKommentare(lies(datei)).match(/kn-fn-[a-z0-9-]+/g) ?? [])) assert.ok(css.includes(`.${k}`), `${datei}: .${k} fehlt im CSS`)
|
||||
})
|
||||
|
||||
// entfernt var(…) samt verschachtelter Klammern: danach darf kein Farbliteral mehr übrig sein (Ausnahme: weiße Schrift auf dem Primärknopf)
|
||||
function ohneVar(text) {
|
||||
let aus = ''
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
if (!text.startsWith('var(', i)) { aus += text[i]; continue }
|
||||
let tiefe = 0
|
||||
let j = i + 3
|
||||
for (; j < text.length; j += 1) {
|
||||
if (text[j] === '(') tiefe += 1
|
||||
else if (text[j] === ')') { tiefe -= 1; if (tiefe === 0) break }
|
||||
}
|
||||
i = j
|
||||
}
|
||||
return aus
|
||||
}
|
||||
|
||||
test('funktionen.css: jedes benutzte Token ist in tokens.css definiert, Farbliterale nur als Fallback von var()', () => {
|
||||
const tokens = lies('public/neu/tokens.css')
|
||||
for (const m of css.matchAll(/var\((--kn-[\w-]+)/g)) assert.ok(tokens.includes(`${m[1]}:`), m[1])
|
||||
assert.doesNotMatch(ohneVar(css.replace(/\/\*[\s\S]*?\*\//g, '')), /#(?!fff\b)[0-9a-f]{3,8}\b|rgba?\(/i)
|
||||
})
|
||||
|
||||
for (const datei of JS) {
|
||||
test(`${datei}: kein innerHTML, nur relative Importe, keine Preise, keine Sie-Anrede`, () => {
|
||||
const code = ohneKommentare(lies(datei))
|
||||
assert.doesNotMatch(code, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|\beval\(|new Function/)
|
||||
for (const m of code.matchAll(/(?:from|import\()\s*'([^']+)'/g)) assert.match(m[1], /^\.{1,2}\//, m[1])
|
||||
assert.doesNotMatch(code, /€|\bEuro\b/)
|
||||
assert.doesNotMatch(code, /\b(Sie|Ihre[rnms]?|Ihnen|Ihr)\b/)
|
||||
})
|
||||
}
|
||||
|
||||
test('beide Module laden unter Node ohne DOM (kein Zugriff beim Import)', async () => {
|
||||
assert.equal(typeof globalThis.document, 'undefined')
|
||||
await import('../public/neu/ansicht-funktionen.js')
|
||||
await import('../public/neu/funktion-einschalten.js')
|
||||
})
|
||||
|
||||
test('Module der Funktionen: keine Hash-Namen außer den vorhandenen, kein Hash mit Großbuchstaben', () => {
|
||||
for (const datei of JS) for (const m of ohneKommentare(lies(datei)).matchAll(/'#([A-Za-z-]+)/g)) assert.match(m[1], /^[a-z-]+$/, `${datei}: #${m[1]}`)
|
||||
})
|
||||
Reference in new issue
Block a user