Kundenansicht neu: Wasser-Leiste breiter mit Wörtern, Unterseiten im Vollbild
Wunsch vom 29.10. nach dem ersten Blick am iPhone: - Die Leiste nutzt die Breite (12 px Rand, höchstens 480 px), Symbol über dem Wort, und steht ganz über dem Wisch-Balken des iPhones (Safe-Area plus 6 px statt minus 10 px). - Unterseiten öffnen sich ganz und werden mit Unschärfe eingeblendet statt hochzufahren. Oben links die Überschrift, oben rechts „Zurück“ als Glasknopf, der beim Scrollen stehen bleibt (neuer Platz #kn-blatt-zurueck). - Reiter einer Funktion stehen unten in derselben Wasser-Leiste (Art 'reiter', Kurzwörter, kleinere Schrift wenn es eng wird), ohne Zurück. Ohne Reiter bleibt unten nichts. Die alte Leiste gibt beim Neuaufbau ihren WebGL-Kontext frei. Glas und Unschärfe stehen weiter allein in wasser-leiste.css; huelle.css bleibt ohne Glas. Desktop unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c997703251
commit
1f350ca432
10 files changed
+147
-95
No files matched your search
@@ -81,7 +81,8 @@ test('M4: der Mitarbeiter-Banner steht oben (sticky), Kopf, Menü, Blatt und Kon
|
||||
assert.match(HUELLE, /\.kn-banner \{[^}]*position:\s*sticky;[^}]*top:\s*0/)
|
||||
assert.match(HUELLE, /\.kn-menue \{[^}]*inset:\s*var\(--kn-banner-h\) auto 0 0/)
|
||||
assert.match(HUELLE, /\.kn-kopf \{[^}]*position:\s*sticky;\s*top:\s*var\(--kn-banner-h\)/)
|
||||
assert.match(HUELLE, /\.kn-blatt \{[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ var\(--kn-kopf-h\) \+ env\(safe-area-inset-top\)\)/)
|
||||
assert.match(HUELLE, /\.kn-blatt \{[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ env\(safe-area-inset-top\)\) 0 0/) // Wasser-Leiste: Unterseite als Vollbild unter dem Banner
|
||||
assert.match(HUELLE, /\.kn-blatt-zurueck \{[^}]*top:\s*calc\(var\(--kn-banner-h\) \+ env\(safe-area-inset-top\)/)
|
||||
assert.match(HUELLE, /\.kn-konto \{[^}]*top:\s*calc\(var\(--kn-banner-h\) \+ 56px\)/)
|
||||
assert.doesNotMatch(HUELLE, /\.kn-banner:not\(\[hidden\]\) ~ \.kn-huelle \.kn-menue/, 'die feste Höhe 36 px ist weg')
|
||||
const js = quelle('huelle.js')
|
||||
|
||||
+22
-13
@@ -5,24 +5,24 @@ import { h } from '../public/neu/dom.js'
|
||||
import { baueEtage } from '../public/neu/etage.js'
|
||||
import { lies, handyTeil } from './support/knHuelle.js'
|
||||
|
||||
const teile = () => ({ blatt: h('div', { hidden: true }, h('section', { 'data-etage': 'termine' })), haupt: h('main'), zeile: h('div'), titel: h('h1'), brot: h('nav'), reiterSlot: h('div') })
|
||||
const teile = () => ({ blatt: h('div', { hidden: true }, h('section', { 'data-etage': 'termine' })), haupt: h('main'), zeile: h('div'), zurueckSlot: h('div'), titel: h('h1'), brot: h('nav'), reiterSlot: h('div') })
|
||||
const REITER = ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen', 'Kalender', 'Rechnungen'].map((label, i) => ({ key: `r${i}`, label, kurz: label.split(' ')[0] }))
|
||||
const EL = { label: 'Funktionen', hash: '#funktionen' }
|
||||
|
||||
test('oeffne und schliesse: Titel, Zurück mit Ziel, Brotkrume, Hauptseite gesperrt', async () => {
|
||||
test('oeffne und schliesse: Titel, Zurück oben rechts mit Ziel, Brotkrume, Hauptseite gesperrt', async () => {
|
||||
const fx = installFakeDom()
|
||||
try {
|
||||
const t = teile(); let zu = 0; const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
||||
e.oeffne({ titel: 'Terminplaner', eltern: EL, seite: 'termine' })
|
||||
assert.deepEqual([t.blatt.hidden, t.haupt.getAttribute('inert'), e.istOffen(), t.titel.textContent], [false, '', true, 'Terminplaner'])
|
||||
assert.deepEqual([fx.text(t.brot), fx.finde(t.brot, 'a').getAttribute('href')], ['Funktionen / Terminplaner', '#funktionen'])
|
||||
const z = fx.finde(t.zeile, 'button'); assert.deepEqual([z.textContent, z.getAttribute('aria-label')], ['Zurück', 'Zurück zu Funktionen'])
|
||||
const z = fx.finde(t.zurueckSlot, 'button'); assert.deepEqual([z.textContent, z.getAttribute('aria-label'), t.zeile.children.length], ['Zurück', 'Zurück zu Funktionen', 0])
|
||||
await fx.klick(z); assert.equal(zu, 1)
|
||||
e.schliesse(); assert.deepEqual([t.blatt.hidden, t.haupt.hasAttribute('inert'), fx.alle(t.zeile, 'button').length], [true, false, 0])
|
||||
e.schliesse(); assert.deepEqual([t.blatt.hidden, t.haupt.hasAttribute('inert'), fx.alle(t.zurueckSlot, 'button').length], [true, false, 0])
|
||||
} finally { fx.restore() }
|
||||
})
|
||||
|
||||
test('kn:reiter vom Inhalt: Handy Kurzlabels in der Zeile, Desktop Unterstrich im Slot, ohne Reiter füllt Zurück die Zeile (C2)', async () => {
|
||||
test('kn:reiter vom Inhalt: Handy Kurzlabels als Wasser-Leiste in der Zeile, Desktop Unterstrich im Slot, ohne Reiter bleibt die Zeile leer (C2)', async () => {
|
||||
const fx = installFakeDom()
|
||||
try {
|
||||
const gew = []; let handy = true; const t = teile(); const e = baueEtage({ ...t, istHandy: () => handy, onZurueck() {} })
|
||||
@@ -30,9 +30,10 @@ test('kn:reiter vom Inhalt: Handy Kurzlabels in der Zeile, Desktop Unterstrich i
|
||||
await fx.sende(kind, 'kn:reiter', { detail: { eintraege: REITER, aktiv: 'r0', bei: (k) => gew.push(k) } })
|
||||
const tabs = fx.alle(t.zeile, '[role=tab]')
|
||||
assert.deepEqual(tabs.map((x) => x.textContent), ['Termine', 'Leistungen', 'Team', 'Einstellungen', 'Kalender', 'Rechnungen']); assert.equal(tabs[2].getAttribute('aria-label'), 'Team & Plätze')
|
||||
assert.deepEqual([fx.finde(t.zeile, '.kn-wl').getAttribute('data-art'), fx.alle(t.zeile, 'button:not([role=tab])').length], ['reiter', 0]) // unten kein Zurück
|
||||
await fx.klick(tabs[2]); assert.deepEqual(gew, ['r2'])
|
||||
await fx.sende(kind, 'kn:reiter', { detail: { eintraege: [], aktiv: '', bei() {} } })
|
||||
assert.deepEqual([fx.alle(t.zeile, '[role=tab]').length, Boolean(fx.finde(t.zeile, '.kn-zurueck--voll'))], [0, true])
|
||||
assert.deepEqual([t.zeile.children.length, fx.alle(t.zurueckSlot, '.kn-zurueck').length], [0, 1])
|
||||
handy = false; e.neuZeichnen(); await fx.sende(kind, 'kn:reiter', { detail: { eintraege: REITER, aktiv: 'r0', bei() {} } })
|
||||
assert.deepEqual([fx.alle(t.reiterSlot, '[role=tab]').length, fx.alle(t.zeile, '[role=tab]').length], [6, 0])
|
||||
await fx.sende(kind, 'kn:titel', { detail: { text: 'Kalender' } }); assert.equal(t.titel.textContent, 'Kalender')
|
||||
@@ -44,14 +45,22 @@ test('Chat-Punkt am Zurück', () => {
|
||||
try {
|
||||
const t = teile(); const e = baueEtage({ ...t, istHandy: () => true, onZurueck() {} })
|
||||
e.oeffne({ titel: 'Webshop', eltern: EL, seite: 'webshop' }); e.setzeChatPunkt(true)
|
||||
assert.ok(fx.finde(t.zeile, '.kn-punkt')); assert.equal(fx.finde(t.zeile, 'button').getAttribute('aria-label'), 'Zurück zu Funktionen, neue Nachricht im Chat')
|
||||
assert.ok(fx.finde(t.zurueckSlot, '.kn-punkt')); assert.equal(fx.finde(t.zurueckSlot, 'button').getAttribute('aria-label'), 'Zurück zu Funktionen, neue Nachricht im Chat')
|
||||
} finally { fx.restore() }
|
||||
})
|
||||
|
||||
test('huelle.css (Blatt): Handy fährt von unten ein, Zurück füllt die Zeile', () => {
|
||||
const css = lies('huelle.css')
|
||||
assert.match(handyTeil(css), /\.kn-blatt\s*\{(?=[^}]*border-radius:\s*12px 12px 0 0)(?=[^}]*transition:[^}]*550ms[^}]*var\(--kn-bremse\))/s)
|
||||
assert.match(css, /\.kn-zurueck--voll\s*\{[^}]*flex:\s*1/s)
|
||||
test('Blatt am Handy: ganzer Bildschirm, mit Unschärfe eingeblendet, Zurück als Glasknopf oben rechts (Glas nur in wasser-leiste.css), Desktop ohne', () => {
|
||||
const css = lies('huelle.css'); const handy = handyTeil(css); const wl = handyTeil(lies('wasser-leiste.css'))
|
||||
assert.match(handy, /\.kn-blatt\s*\{(?=[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ env\(safe-area-inset-top\)\) 0 0)(?=[^}]*opacity:\s*0)/s)
|
||||
assert.doesNotMatch(handy, /translateY\(100%\)\s*;\s*transition:\s*transform 550ms/) // fährt nicht mehr hoch
|
||||
assert.match(handy, /\.kn-blatt-zurueck\s*\{(?=[^}]*position:\s*fixed)(?=[^}]*right:\s*max\(16px, env\(safe-area-inset-right\)\))/s)
|
||||
assert.match(handy, /\.kn-blatt--auf ~ \.kn-blatt-zurueck\s*\{[^}]*visibility:\s*visible/)
|
||||
assert.match(css.slice(0, css.indexOf('@media')), /\.kn-blatt-zurueck\s*\{\s*display:\s*none/) // Desktop: Brotkrume statt Zurück
|
||||
assert.match(wl, /\.kn-blatt\s*\{\s*filter:\s*blur\(14px\);\s*transition:[^}]*filter/)
|
||||
assert.match(wl, /\.kn-blatt--auf\s*\{\s*filter:\s*none/)
|
||||
assert.match(wl, /\.kn-blatt-zurueck \.kn-zurueck\s*\{[^}]*backdrop-filter:\s*blur/)
|
||||
assert.match(lies('wasser-leiste.css'), /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.kn-blatt\s*\{\s*filter:\s*none/)
|
||||
assert.match(lies('dashboard.html'), /<\/div>\s*<div id="kn-blatt-zurueck" class="kn-blatt-zurueck"><\/div>/) // Geschwister nach dem Blatt (Selektor ~)
|
||||
})
|
||||
|
||||
test('Etage: Brotkrume führt wie Zurück, Chat-Punkt bleibt für die nächste Etage, Reiter anderer Seiten und Reste werden verworfen, gleiche Reiter bleiben stehen', async () => {
|
||||
@@ -60,7 +69,7 @@ test('Etage: Brotkrume führt wie Zurück, Chat-Punkt bleibt für die nächste E
|
||||
const t = teile(); t.blatt.append(h('section', { 'data-etage': 'webshop' })); let zu = 0
|
||||
const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
||||
e.setzeChatPunkt(true) // vor dem Öffnen gemerkt
|
||||
e.oeffne({ titel: 'Terminplaner', eltern: EL, seite: 'termine' }); assert.ok(fx.finde(t.zeile, '.kn-punkt'))
|
||||
e.oeffne({ titel: 'Terminplaner', eltern: EL, seite: 'termine' }); assert.ok(fx.finde(t.zurueckSlot, '.kn-punkt'))
|
||||
assert.deepEqual(t.blatt.children.map((s) => s.hidden), [false, true]) // nur die Seite der Etage ist sichtbar
|
||||
await fx.klick(fx.finde(t.brot, 'a'))
|
||||
assert.equal(zu, 1) // Klick auf die Brotkrume: wie Zurück, ohne Fragment-Navigation
|
||||
@@ -72,7 +81,7 @@ test('Etage: Brotkrume führt wie Zurück, Chat-Punkt bleibt für die nächste E
|
||||
await fx.sende(termine, 'kn:reiter', { detail: { eintraege: REITER, aktiv: 'r3', bei() {} } })
|
||||
assert.deepEqual([fx.finde(t.zeile, '[role=tablist]') === erster, fx.alle(t.zeile, '[role=tab]').map((x) => x.getAttribute('aria-selected')).indexOf('true')], [true, 3]) // gleiche Reiter: nur die Auswahl wechselt
|
||||
e.oeffne({ titel: 'Webshop', eltern: EL, seite: 'webshop' })
|
||||
assert.deepEqual([fx.alle(t.zeile, '[role=tab]').length, Boolean(fx.finde(t.zeile, '.kn-zurueck--voll')), Boolean(fx.finde(t.zeile, '.kn-punkt'))], [0, true, true]) // neue Etage: alte Reiter weg, Punkt bleibt
|
||||
assert.deepEqual([fx.alle(t.zeile, '[role=tab]').length, Boolean(fx.finde(t.zurueckSlot, '.kn-zurueck')), Boolean(fx.finde(t.zurueckSlot, '.kn-punkt'))], [0, true, true]) // neue Etage: alte Reiter weg, Punkt bleibt
|
||||
e.schliesse(); await fx.sende(termine, 'kn:reiter', { detail: { eintraege: REITER, aktiv: 'r0', bei() {} } })
|
||||
assert.equal(fx.alle(t.zeile, '[role=tab]').length, 0) // geschlossen: späte Meldungen tun nichts
|
||||
} finally { fx.restore() }
|
||||
|
||||
@@ -76,7 +76,7 @@ test('Etage Terminplaner: wunschProjekt aus ?projekt=, Modul auf #termine-body m
|
||||
const seen = []
|
||||
const b = await boot({ hash: '#termine?bestellt=1&projekt=p7', karte: { ...KARTE_KUNDE, '/api/chat/unread-count': { unread: 3 } }, lade: async () => ({ initTermine: (root, opt) => seen.push([root.id, root.dataset.wunschProjekt, typeof opt?.einschalten]) }) })
|
||||
assert.deepEqual(seen, [['termine-body', 'p7', 'function']]); assert.ok(b.log.some((l) => l[0] === 'stempeln'))
|
||||
assert.deepEqual([b.el('kn-blatt').hidden, b.el('kn-etage-titel').textContent, Boolean(b.fx.finde(b.el('kn-zeile-etage'), '.kn-punkt'))], [false, 'Terminplaner', true])
|
||||
assert.deepEqual([b.el('kn-blatt').hidden, b.el('kn-etage-titel').textContent, Boolean(b.fx.finde(b.el('kn-blatt-zurueck'), '.kn-punkt'))], [false, 'Terminplaner', true])
|
||||
const x = await boot({ hash: '#webshop', karte: KARTE_KUNDE, lade: async () => { throw new Error('404') } })
|
||||
assert.equal(x.el('webshop-body').textContent, 'Konnte nicht geladen werden.')
|
||||
})
|
||||
|
||||
@@ -90,6 +90,20 @@ test('Bauen: vier Reiter mit Wort, Symbol-Platz und Szene, Auswahl, Zähler am S
|
||||
} finally { fx.restore() }
|
||||
})
|
||||
|
||||
test('Art reiter: nur Wörter (Kurzwort sichtbar, volles Wort vorgelesen), keine Szene, kein Zähler; beende räumt auf', async () => {
|
||||
const fx = installFakeDom()
|
||||
try {
|
||||
const gew = []
|
||||
const w = baueWasserLeiste({ eintraege: [{ key: 'termine', label: 'Termine' }, { key: 'team', label: 'Team & Plätze', kurz: 'Team' }], aktiv: 'termine', art: 'reiter', ariaLabel: 'Reiter', bei: (k) => gew.push(k) })
|
||||
const tabs = fx.alle(w, '[role=tab]')
|
||||
assert.deepEqual([w.getAttribute('data-art'), w.getAttribute('aria-label'), fx.alle(w, '.kn-wl-ico').length], ['reiter', 'Reiter', 0])
|
||||
assert.deepEqual(tabs.map((t) => [t.textContent, t.getAttribute('aria-label')]), [['Termine', 'Termine'], ['Team', 'Team & Plätze']])
|
||||
await fx.klick(tabs[1]); assert.deepEqual([gew, tabs[1].getAttribute('aria-selected')], [['team'], 'true'])
|
||||
w.setzeAnzeige('team', { zahl: 2 }); assert.equal(fx.finde(w, '.kn-zahl'), null)
|
||||
w.beende(); w.beende() // zweimal ist kein Fehler
|
||||
} finally { fx.restore() }
|
||||
})
|
||||
|
||||
test('Symbole und Szenen: für jeden Bereich der Leiste ein Symbol und eine eigene Szene', () => {
|
||||
for (const { key } of WOERTER) { assert.ok(SYMBOLE[key]?.length > 0, key); assert.ok(SZENEN[key], key) }
|
||||
assert.equal(new Set(WOERTER.map((w) => SZENEN[w.key])).size, WOERTER.length)
|
||||
@@ -100,15 +114,18 @@ test('wasser-leiste.css: Pille wie die Vorlage, am Handy schwebend über dem Inh
|
||||
const css = lies('wasser-leiste.css'); const handy = handyTeil(css)
|
||||
assert.match(css, /\.kn-wl-glas\s*\{[^}]*backdrop-filter:\s*blur\(10px\) saturate\(1\.2\) brightness\(1\.08\)/s)
|
||||
assert.match(css, /\.kn-wl\[data-gl="off"\] \.kn-wl-tropfen\s*\{\s*display:\s*block/)
|
||||
assert.match(css, /@media \(max-width: 520px\)\s*\{[^@]*\.kn-wl-label\s*\{[^}]*clip:\s*rect\(0 0 0 0\)/s) // schmal nur Symbole, Wort bleibt für Screenreader
|
||||
assert.doesNotMatch(css, /max-width: 520px/) // die Wörter stehen immer da (Wunsch 29.10.)
|
||||
assert.match(css, /\.kn-wl\[data-art="bereiche"\] \.kn-wl-item\s*\{[^}]*flex:\s*1 1 0;[^}]*flex-direction:\s*column/) // Symbol über dem Wort, gleich breit
|
||||
assert.match(css, /\.kn-wl\[data-art="reiter"\]\[data-eng\] \.kn-wl-item\s*\{[^}]*font-size:\s*12px/)
|
||||
for (const fx of ['pop', 'plane', 'drop', 'spin', 'glow', 'wave']) assert.match(css, new RegExp(`\\.kn-wl-ico\\[data-fx="${fx}"\\] svg\\s*\\{[^}]*animation:`))
|
||||
assert.ok(handy.startsWith(HANDY))
|
||||
assert.match(handy, /\.kn\s*\{[^}]*--kn-wl-luft:\s*max\(10px, calc\(env\(safe-area-inset-bottom\) - 10px\)\)[^}]*--kn-unten:\s*var\(--kn-wl-zone\)/s)
|
||||
assert.match(handy, /\.kn\s*\{[^}]*--kn-wl-luft:\s*max\(14px, calc\(env\(safe-area-inset-bottom\) \+ 6px\)\)[^}]*--kn-unten:\s*var\(--kn-wl-zone\)/s) // ganz über dem Wisch-Balken des iPhones
|
||||
assert.match(handy, /\.kn\.kn-hat-streifen\s*\{\s*--kn-unten:\s*calc\(var\(--kn-wl-zone\) \+ 44px\)/)
|
||||
assert.ok(handy.indexOf('.kn.kn-tastatur') > handy.indexOf('.kn.kn-hat-streifen')) // Tastatur schlägt den Streifen
|
||||
assert.match(handy, /\.kn-leiste\s*\{[^}]*background:\s*none[^}]*pointer-events:\s*none/s)
|
||||
assert.match(handy, /\.kn-blatt\s*\{\s*bottom:\s*0;\s*padding-bottom:\s*calc\(var\(--kn-unten\) \+ 24px\)/)
|
||||
assert.match(handy, /#kn-zeile-etage\s*\{[^}]*border-radius:\s*999px[^}]*backdrop-filter/s)
|
||||
assert.match(handy, /\.kn-leiste \.kn-rw\s*\{[^}]*right:\s*12px;[^}]*left:\s*12px;[^}]*max-width:\s*480px/s) // breit: Rand 12 px, höchstens 480 px
|
||||
assert.match(handy, /\.kn-leiste \.kn-wl\s*\{\s*display:\s*flex;\s*width:\s*100%/)
|
||||
assert.match(handy, /\.kn-leiste \.kn-rw\s*\{[^}]*pointer-events:\s*none/s); assert.match(handy, /\.kn-leiste \.kn-rw > \*\s*\{\s*pointer-events:\s*auto/)
|
||||
assert.match(css, /@media \(prefers-reduced-motion: reduce\)\s*\{[^@]*animation:\s*none !important/s)
|
||||
// huelle.css bleibt unverändert die Grundlage (Desktop und Etage-Logik)
|
||||
assert.match(lies('huelle.css'), /\.kn-leiste\s*\{[^}]*border-top:\s*1px solid var\(--kn-line\)/)
|
||||
|
||||
Reference in new issue
Block a user