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
@@ -73,6 +73,7 @@
|
|||||||
<section data-etage="analyse" hidden><div id="analyse-body"></div></section>
|
<section data-etage="analyse" hidden><div id="analyse-body"></div></section>
|
||||||
<section data-etage="seite" hidden><div id="etage-body"></div></section>
|
<section data-etage="seite" hidden><div id="etage-body"></div></section>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="kn-blatt-zurueck" class="kn-blatt-zurueck"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+19
-15
@@ -1,29 +1,32 @@
|
|||||||
/* Kundenansicht neu: Etage 2. Eine Unterseite (Funktion, Detailseite, Passwort …) schiebt sich als Blatt über die Hauptseite.
|
/* Kundenansicht neu: Etage 2. Eine Unterseite (Funktion, Detailseite, Passwort …) legt sich als Blatt über die Hauptseite.
|
||||||
Handy: Die Zeile unten fährt eine Etage hoch und zeigt „Zurück“ und, falls die Seite Reiter meldet, deren Kurzlabels als Balken.
|
Handy: Das Blatt deckt den ganzen Bildschirm, „Zurück“ steht oben rechts (zurueckSlot), und meldet die Seite Reiter, zeigt die Zeile unten
|
||||||
Desktop: Brotkrume statt Zeile, Reiter als Unterstrich im Slot unter der Überschrift.
|
deren Kurzlabels als Wasser-Leiste (wasser-leiste.js, Art 'reiter') an Stelle der vier Bereiche; ohne Reiter bleibt unten nichts.
|
||||||
|
Desktop: Brotkrume statt Zurück, Reiter als Unterstrich im Slot unter der Überschrift.
|
||||||
Der Inhalt meldet Reiter und Titel ohne Import per CustomEvent (bubbles): kn:reiter { eintraege, aktiv, bei } (leere Liste = keine Reiter)
|
Der Inhalt meldet Reiter und Titel ohne Import per CustomEvent (bubbles): kn:reiter { eintraege, aktiv, bei } (leere Liste = keine Reiter)
|
||||||
und kn:titel { text }. Alles Sichtbare wird mit h() und textContent gebaut. */
|
und kn:titel { text }. Alles Sichtbare wird mit h() und textContent gebaut. */
|
||||||
|
|
||||||
import { h, leeren, text } from './dom.js'
|
import { h, leeren, text } from './dom.js'
|
||||||
import { baueReiter } from './reiter.js'
|
import { baueReiter } from './reiter.js'
|
||||||
|
import { baueWasserLeiste } from './wasser-leiste.js'
|
||||||
|
|
||||||
const gleicheEintraege = (a, b) => a.length === b.length && a.every((x, i) => x.key === b[i].key && x.label === b[i].label && (x.kurz ?? '') === (b[i].kurz ?? ''))
|
const gleicheEintraege = (a, b) => a.length === b.length && a.every((x, i) => x.key === b[i].key && x.label === b[i].label && (x.kurz ?? '') === (b[i].kurz ?? ''))
|
||||||
|
|
||||||
export function baueEtage({ blatt, haupt, zeile, titel, brot, reiterSlot, istHandy, onZurueck }) {
|
export function baueEtage({ blatt, haupt, zeile, zurueckSlot, titel, brot, reiterSlot, istHandy, onZurueck }) {
|
||||||
const stand = { offen: false, titel: '', eltern: null, seite: '', reiter: null, chatPunkt: false, reiterEl: null, zurueckEl: null }
|
const stand = { offen: false, titel: '', eltern: null, seite: '', reiter: null, chatPunkt: false, reiterEl: null, zurueckEl: null }
|
||||||
|
|
||||||
const zurueckLabel = () => `Zurück zu ${stand.eltern.label}${stand.chatPunkt ? ', neue Nachricht im Chat' : ''}`
|
const zurueckLabel = () => `Zurück zu ${stand.eltern.label}${stand.chatPunkt ? ', neue Nachricht im Chat' : ''}`
|
||||||
|
|
||||||
function baueZurueck(voll) {
|
function baueZurueck() {
|
||||||
return h('button', { class: voll ? 'kn-zurueck kn-zurueck--voll' : 'kn-zurueck', type: 'button', 'aria-label': zurueckLabel(), on: { click: () => onZurueck() } },
|
return h('button', { class: 'kn-zurueck', type: 'button', 'aria-label': zurueckLabel(), on: { click: () => onZurueck() } },
|
||||||
h('span', { class: 'kn-l' }, 'Zurück', stand.chatPunkt ? h('span', { class: 'kn-punkt' }) : null))
|
h('span', { class: 'kn-l' }, 'Zurück', stand.chatPunkt ? h('span', { class: 'kn-punkt' }) : null))
|
||||||
}
|
}
|
||||||
|
|
||||||
function neuerReiter(art) {
|
function neuerReiter(art) {
|
||||||
const { eintraege, aktiv } = stand.reiter
|
const { eintraege, aktiv } = stand.reiter
|
||||||
// Handy zeigt die Kurzlabels (der Balken hat wenig Platz), Desktop die vollen Beschriftungen
|
const bei = (key) => stand.reiter?.bei?.(key)
|
||||||
const zeigen = art === 'balken' ? eintraege : eintraege.map(({ kurz, ...rest }) => rest)
|
// Handy: Wasser-Leiste mit den Kurzlabels (vorgelesen wird das volle Wort); Desktop: Unterstrich mit den vollen Beschriftungen
|
||||||
return baueReiter({ eintraege: zeigen, aktiv, art, ariaLabel: 'Reiter', bei: (key) => stand.reiter?.bei?.(key) })
|
if (art === 'wasser') return baueWasserLeiste({ eintraege: eintraege.map(({ key, label, kurz }) => ({ key, label, kurz })), aktiv, art: 'reiter', ariaLabel: 'Reiter', bei })
|
||||||
|
return baueReiter({ eintraege: eintraege.map(({ kurz, ...rest }) => rest), aktiv, art, ariaLabel: 'Reiter', bei })
|
||||||
}
|
}
|
||||||
|
|
||||||
function zeichneBrot() {
|
function zeichneBrot() {
|
||||||
@@ -37,18 +40,19 @@ export function baueEtage({ blatt, haupt, zeile, titel, brot, reiterSlot, istHan
|
|||||||
brot.append(h('a', { href: stand.eltern.hash, on: { click: verlassen } }, stand.eltern.label), h('span', { class: 'kn-brot-trenner', 'aria-hidden': 'true' }, ' / '), h('span', { 'aria-current': 'page' }, stand.titel))
|
brot.append(h('a', { href: stand.eltern.hash, on: { click: verlassen } }, stand.eltern.label), h('span', { class: 'kn-brot-trenner', 'aria-hidden': 'true' }, ' / '), h('span', { 'aria-current': 'page' }, stand.titel))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeile (Handy) und Reiter-Slot (Desktop) aus dem Stand neu aufbauen
|
// Zurück und Zeile (Handy) bzw. Reiter-Slot (Desktop) aus dem Stand neu aufbauen
|
||||||
function zeichne() {
|
function zeichne() {
|
||||||
leeren(zeile); leeren(reiterSlot)
|
stand.reiterEl?.beende?.() // die alte Wasser-Leiste gibt ihren WebGL-Kontext frei
|
||||||
|
leeren(zeile); leeren(reiterSlot); if (zurueckSlot) leeren(zurueckSlot)
|
||||||
stand.zurueckEl = null; stand.reiterEl = null
|
stand.zurueckEl = null; stand.reiterEl = null
|
||||||
zeichneBrot()
|
zeichneBrot()
|
||||||
if (!stand.offen) return
|
if (!stand.offen) return
|
||||||
if (istHandy()) {
|
if (istHandy()) {
|
||||||
stand.zurueckEl = baueZurueck(!stand.reiter)
|
stand.zurueckEl = baueZurueck()
|
||||||
zeile.append(stand.zurueckEl)
|
;(zurueckSlot ?? zeile).append(stand.zurueckEl)
|
||||||
if (stand.reiter) {
|
if (stand.reiter) {
|
||||||
stand.reiterEl = neuerReiter('balken')
|
stand.reiterEl = neuerReiter('wasser')
|
||||||
zeile.append(h('span', { class: 'kn-vl' }), stand.reiterEl)
|
zeile.append(stand.reiterEl)
|
||||||
}
|
}
|
||||||
} else if (stand.reiter) {
|
} else if (stand.reiter) {
|
||||||
stand.reiterEl = neuerReiter('unterstrich')
|
stand.reiterEl = neuerReiter('unterstrich')
|
||||||
|
|||||||
+23
-10
@@ -1,4 +1,5 @@
|
|||||||
/* Kundenansicht neu: Hülle (Menü, Kopf, Blatt, Leiste, Streifen, Konto). Weiß, Haarlinie, Schwarz; keine Verläufe, kein Glas.
|
/* Kundenansicht neu: Hülle (Menü, Kopf, Blatt, Leiste, Streifen, Konto). Weiß, Haarlinie, Schwarz; keine Verläufe, kein Glas (das Glas der Handy-Leiste
|
||||||
|
und des Zurück-Knopfs steht allein in wasser-leiste.css).
|
||||||
Die Grundregeln gelten am Desktop, die Handy-Abfrage überschreibt für das Handy. */
|
Die Grundregeln gelten am Desktop, die Handy-Abfrage überschreibt für das Handy. */
|
||||||
|
|
||||||
/* ---------- Blatt (Etage 2) ---------- */
|
/* ---------- Blatt (Etage 2) ---------- */
|
||||||
@@ -13,20 +14,28 @@
|
|||||||
|
|
||||||
.kn-zurueck { position: relative; display: flex; align-items: center; gap: 6px; height: 44px; padding: 0 8px 0 6px; font-size: 15px; font-weight: 600; color: var(--kn-text); }
|
.kn-zurueck { position: relative; display: flex; align-items: center; gap: 6px; height: 44px; padding: 0 8px 0 6px; font-size: 15px; font-weight: 600; color: var(--kn-text); }
|
||||||
.kn-zurueck::before { content: ""; width: 9px; height: 9px; margin-left: 4px; border: solid; border-width: 0 0 2px 2px; transform: rotate(45deg); }
|
.kn-zurueck::before { content: ""; width: 9px; height: 9px; margin-left: 4px; border: solid; border-width: 0 0 2px 2px; transform: rotate(45deg); }
|
||||||
.kn-zurueck--voll { flex: 1; }
|
|
||||||
.kn-zurueck .kn-l { position: relative; line-height: 20px; }
|
.kn-zurueck .kn-l { position: relative; line-height: 20px; }
|
||||||
.kn-vl { flex: none; width: 1px; height: 24px; margin: 0 4px; background: var(--kn-line); }
|
.kn-blatt-zurueck { display: none; } /* „Zurück“ der Unterseite, nur am Handy (etage.js); der Desktop hat die Brotkrume */
|
||||||
.kn-punkt { position: absolute; top: -2px; left: 100%; width: 9px; height: 9px; margin-left: 2px; border-radius: 50%; background: #b3093c; box-shadow: 0 0 0 2px var(--kn-bg); }
|
.kn-punkt { position: absolute; top: -2px; left: 100%; width: 9px; height: 9px; margin-left: 2px; border-radius: 50%; background: #b3093c; box-shadow: 0 0 0 2px var(--kn-bg); }
|
||||||
|
|
||||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||||
.kn { --kn-unten: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom)); }
|
.kn { --kn-unten: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom)); }
|
||||||
|
/* Die Unterseite öffnet sich ganz (unter der Statusleiste) und wird eingeblendet (die Unschärfe dazu in wasser-leiste.css). Oben links die
|
||||||
|
Überschrift, oben rechts „Zurück“; er bleibt beim Scrollen stehen. Reiter stehen unten in der Wasser-Leiste. */
|
||||||
|
.kn { --kn-zurueck-oben: 17px; } /* so steht der 40 px hohe Knopf auf der Mitte der ersten Zeile der Überschrift */
|
||||||
.kn-blatt {
|
.kn-blatt {
|
||||||
position: fixed; inset: calc(var(--kn-banner-h) + var(--kn-kopf-h) + env(safe-area-inset-top)) 0 var(--kn-unten, var(--kn-zeile-h)); z-index: 20;
|
position: fixed; inset: calc(var(--kn-banner-h) + env(safe-area-inset-top)) 0 0; z-index: 20; overflow-y: auto; overscroll-behavior: contain;
|
||||||
overflow-y: auto; overscroll-behavior: contain; border-radius: 12px 12px 0 0;
|
opacity: 0; transition: opacity 380ms ease;
|
||||||
box-shadow: 0 0 0 1px rgba(26,31,54,.06), 0 -8px 24px rgba(26,31,54,.1);
|
|
||||||
transform: translateY(100%); transition: transform 550ms var(--kn-bremse);
|
|
||||||
}
|
}
|
||||||
.kn-blatt--auf { transform: none; }
|
.kn-blatt--auf { opacity: 1; }
|
||||||
|
.kn-blatt > .kn-titel { padding-right: 108px; }
|
||||||
|
.kn-blatt-zurueck {
|
||||||
|
display: block; position: fixed; top: calc(var(--kn-banner-h) + env(safe-area-inset-top) + var(--kn-zurueck-oben)); right: max(16px, env(safe-area-inset-right)); z-index: 25;
|
||||||
|
visibility: hidden; opacity: 0; transition: opacity 380ms ease, visibility 0s 380ms;
|
||||||
|
}
|
||||||
|
.kn-blatt--auf ~ .kn-blatt-zurueck { visibility: visible; opacity: 1; transition-delay: 0s; }
|
||||||
|
.kn-blatt-zurueck .kn-zurueck { height: 40px; padding: 0 16px 0 10px; border-radius: 999px; background: var(--kn-bg); box-shadow: var(--kn-shadow-pop); }
|
||||||
|
.kn .kn-blatt-zurueck .kn-zurueck:focus-visible { outline-offset: 2px; }
|
||||||
.kn-haupt[inert] { display: block; } /* am Handy bleibt die Hauptseite unter dem Blatt stehen */
|
.kn-haupt[inert] { display: block; } /* am Handy bleibt die Hauptseite unter dem Blatt stehen */
|
||||||
.kn-brot { display: none; }
|
.kn-brot { display: none; }
|
||||||
}
|
}
|
||||||
@@ -131,7 +140,7 @@
|
|||||||
.kn-kopf-titel { display: block; font-size: 15px; line-height: 22px; font-weight: 600; opacity: 0; transform: translateY(5px); transition: opacity 250ms, transform 250ms; }
|
.kn-kopf-titel { display: block; font-size: 15px; line-height: 22px; font-weight: 600; opacity: 0; transform: translateY(5px); transition: opacity 250ms, transform 250ms; }
|
||||||
.kn-titled .kn-kopf-titel { opacity: 1; transform: none; }
|
.kn-titled .kn-kopf-titel { opacity: 1; transform: none; }
|
||||||
.kn-haupt { max-width: none; padding: 2px max(16px, env(safe-area-inset-right)) calc(var(--kn-zeile-h) + env(safe-area-inset-bottom) + 28px) max(16px, env(safe-area-inset-left)); }
|
.kn-haupt { max-width: none; padding: 2px max(16px, env(safe-area-inset-right)) calc(var(--kn-zeile-h) + env(safe-area-inset-bottom) + 28px) max(16px, env(safe-area-inset-left)); }
|
||||||
.kn-blatt { max-width: none; padding: 0 max(16px, env(safe-area-inset-right)) 24px max(16px, env(safe-area-inset-left)); }
|
.kn-blatt { max-width: none; padding: 14px max(16px, env(safe-area-inset-right)) calc(var(--kn-unten, var(--kn-zeile-h)) + 24px) max(16px, env(safe-area-inset-left)); }
|
||||||
.kn-haupt .kn-titel, .kn-blatt .kn-titel { margin: 6px 0 10px; font-size: 28px; line-height: 34px; }
|
.kn-haupt .kn-titel, .kn-blatt .kn-titel { margin: 6px 0 10px; font-size: 28px; line-height: 34px; }
|
||||||
.kn-banner button { min-height: 44px; }
|
.kn-banner button { min-height: 44px; }
|
||||||
#kn-fehler { margin: 12px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); }
|
#kn-fehler { margin: 12px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); }
|
||||||
@@ -204,7 +213,9 @@
|
|||||||
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
|
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
|
||||||
.kn { --kn-zeile-h: 44px; --kn-kopf-h: 44px; }
|
.kn { --kn-zeile-h: 44px; --kn-kopf-h: 44px; }
|
||||||
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: translateY(-44px); }
|
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: translateY(-44px); }
|
||||||
.kn-haupt .kn-titel, .kn-blatt .kn-titel { display: none; }
|
.kn-haupt .kn-titel { display: none; }
|
||||||
|
.kn-blatt .kn-titel { margin: 0 0 6px; font-size: 20px; line-height: 26px; } /* die Unterseite deckt den Kopf ab: ihre Überschrift bleibt, kleiner */
|
||||||
|
.kn { --kn-zurueck-oben: 7px; }
|
||||||
.kn-kopf-titel { opacity: 1; transform: none; }
|
.kn-kopf-titel { opacity: 1; transform: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +228,8 @@
|
|||||||
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { transition: none; }
|
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { transition: none; }
|
||||||
.kn-blatt { transform: none; opacity: 0; transition: opacity 150ms; }
|
.kn-blatt { transform: none; opacity: 0; transition: opacity 150ms; }
|
||||||
.kn-blatt--auf { opacity: 1; }
|
.kn-blatt--auf { opacity: 1; }
|
||||||
|
.kn-blatt-zurueck { transition: opacity 150ms, visibility 0s 150ms; }
|
||||||
|
.kn-blatt--auf ~ .kn-blatt-zurueck { transition: opacity 150ms; }
|
||||||
.kn-konto-scrim, .kn-konto { animation: kn-einblenden 150ms ease-out; }
|
.kn-konto-scrim, .kn-konto { animation: kn-einblenden 150ms ease-out; }
|
||||||
.kn-zahl { animation: none; }
|
.kn-zahl { animation: none; }
|
||||||
.kn-zahl--weg { transition: none; }
|
.kn-zahl--weg { transition: none; }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { erzeugeZaehler } from './zaehler.js'
|
|||||||
import { baueStreifen, ladeFristen, ladeFristenNeu } from './fristen-streifen.js'
|
import { baueStreifen, ladeFristen, ladeFristenNeu } from './fristen-streifen.js'
|
||||||
import { baueAvatar, baueKonto, installAngebot } from './konto.js'
|
import { baueAvatar, baueKonto, installAngebot } from './konto.js'
|
||||||
|
|
||||||
export const GERUEST_IDS = ['kn-banner', 'kn-ansage', 'kn-fehler', 'kn-menue', 'kn-kopf', 'kn-kopf-titel', 'kn-kopf-konto', 'kn-inhalt', 'bereich-website', 'bereich-funktionen', 'bereich-chat', 'bereich-vertraege', 'website-body', 'funktionen-body', 'chat-body', 'vertraege-body', 'kn-blatt', 'kn-brot', 'kn-etage-titel', 'kn-reiter-slot', 'news-body', 'termine-body', 'webshop-body', 'buchungen-body', 'zeiten-body', 'analyse-body', 'etage-body', 'kn-streifen', 'kn-leiste', 'kn-zeile-haupt', 'kn-zeile-etage', 'kn-konto-wurzel']
|
export const GERUEST_IDS = ['kn-banner', 'kn-ansage', 'kn-fehler', 'kn-menue', 'kn-kopf', 'kn-kopf-titel', 'kn-kopf-konto', 'kn-inhalt', 'bereich-website', 'bereich-funktionen', 'bereich-chat', 'bereich-vertraege', 'website-body', 'funktionen-body', 'chat-body', 'vertraege-body', 'kn-blatt', 'kn-brot', 'kn-etage-titel', 'kn-reiter-slot', 'news-body', 'termine-body', 'webshop-body', 'buchungen-body', 'zeiten-body', 'analyse-body', 'etage-body', 'kn-streifen', 'kn-leiste', 'kn-zeile-haupt', 'kn-zeile-etage', 'kn-konto-wurzel', 'kn-blatt-zurueck']
|
||||||
|
|
||||||
// Bereich → [Modul, Funktion, Wurzel]; die Ansicht heißt ansicht-<name>.js und exportiert zeige<Name>
|
// Bereich → [Modul, Funktion, Wurzel]; die Ansicht heißt ansicht-<name>.js und exportiert zeige<Name>
|
||||||
export const ANSICHTEN = {
|
export const ANSICHTEN = {
|
||||||
@@ -128,7 +128,7 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan
|
|||||||
const streifen = baueStreifen(el('kn-streifen'), { koerper: doc.body })
|
const streifen = baueStreifen(el('kn-streifen'), { koerper: doc.body })
|
||||||
streifen.setze(null)
|
streifen.setze(null)
|
||||||
const navigation = baueNavigation({ haupt: el('kn-zeile-haupt'), menue: el('kn-menue'), onWahl: (bereich) => router.geheZu(hashFuerRoute({ bereich })) })
|
const navigation = baueNavigation({ haupt: el('kn-zeile-haupt'), menue: el('kn-menue'), onWahl: (bereich) => router.geheZu(hashFuerRoute({ bereich })) })
|
||||||
const etage = baueEtage({ blatt: el('kn-blatt'), haupt: el('kn-inhalt'), zeile: el('kn-zeile-etage'), titel: el('kn-etage-titel'), brot: el('kn-brot'), reiterSlot: el('kn-reiter-slot'), istHandy, onZurueck: () => router.zurueck() })
|
const etage = baueEtage({ blatt: el('kn-blatt'), haupt: el('kn-inhalt'), zeile: el('kn-zeile-etage'), zurueckSlot: el('kn-blatt-zurueck'), titel: el('kn-etage-titel'), brot: el('kn-brot'), reiterSlot: el('kn-reiter-slot'), istHandy, onZurueck: () => router.zurueck() })
|
||||||
let aktuell = null // die Route, die gerade gezeigt wird
|
let aktuell = null // die Route, die gerade gezeigt wird
|
||||||
let herkunft = '' // Hash der Route davor (Rückweg der Passwort-Seite)
|
let herkunft = '' // Hash der Route davor (Rückweg der Passwort-Seite)
|
||||||
const zaehler = erzeugeZaehler({
|
const zaehler = erzeugeZaehler({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* Kundenansicht neu: Wasser-Leiste (wasser-leiste.js). Pille aus Glas mit Wassertropfen, nach der Komponente „Nimbus UI · Liquid Navbar“
|
/* Kundenansicht neu: Wasser-Leiste (wasser-leiste.js). Pille aus Glas mit Wassertropfen, nach der Komponente „Nimbus UI · Liquid Navbar“
|
||||||
(guglielmogiannattasio.it). Die Regeln der Pille sind die der Vorlage mit kn-wl-Namen; darunter der Einbau in die Hülle am Handy:
|
(guglielmogiannattasio.it). Die Regeln der Pille sind die der Vorlage mit kn-wl-Namen; darunter die zwei Arten und der Einbau in die Hülle
|
||||||
Die Leiste ist keine weiße Zeile mehr, sondern eine schwebende Pille über dem Inhalt. Unterseiten (Etage 2) zeigen statt der Bereiche eine
|
am Handy: Die Leiste ist keine weiße Zeile mehr, sondern eine breite schwebende Pille über dem Inhalt. Hauptseiten zeigen die vier Bereiche
|
||||||
gläserne Pille mit „Zurück“ und Reitern; der aktive Reiter liegt in einem Tropfen wie der CSS-Tropfen der Pille. Lädt nach huelle.css. */
|
(Symbol über dem Wort), Unterseiten mit Reitern deren Wörter in derselben Pille; „Zurück“ steht dort oben rechts (huelle.css). Lädt nach huelle.css. */
|
||||||
|
|
||||||
/* ---------- Pille ---------- */
|
/* ---------- Pille ---------- */
|
||||||
|
|
||||||
@@ -69,66 +69,59 @@
|
|||||||
.kn-wl-ico[data-fx="wave"] svg { animation: kn-wl-wink .9s ease-in-out both; transform-origin: 50% 92%; }
|
.kn-wl-ico[data-fx="wave"] svg { animation: kn-wl-wink .9s ease-in-out both; transform-origin: 50% 92%; }
|
||||||
@keyframes kn-wl-wink { 0% { transform: none; } 20% { transform: rotate(-15deg); } 40% { transform: rotate(12deg); } 60% { transform: rotate(-8deg); } 80% { transform: rotate(4deg); } 100% { transform: translateY(-1px) scale(1.08); } }
|
@keyframes kn-wl-wink { 0% { transform: none; } 20% { transform: rotate(-15deg); } 40% { transform: rotate(12deg); } 60% { transform: rotate(-8deg); } 80% { transform: rotate(4deg); } 100% { transform: translateY(-1px) scale(1.08); } }
|
||||||
|
|
||||||
/* Schmal (wie die Vorlage unter 520 px): nur Symbole, die Wörter bleiben für Screenreader im Knopf */
|
/* ---------- Zwei Arten ---------- */
|
||||||
@media (max-width: 520px) {
|
|
||||||
.kn .kn-wl-item { padding: 0 13px; }
|
/* Bereiche (Hauptleiste): Symbol über dem Wort, alle gleich breit */
|
||||||
.kn-wl-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
.kn-wl[data-art="bereiche"] .kn-wl-item { flex: 1 1 0; flex-direction: column; justify-content: center; gap: 4px; min-width: 0; height: 52px; padding: 0 4px; font-size: 11px; line-height: 13px; letter-spacing: 0; }
|
||||||
}
|
.kn-wl[data-art="bereiche"] .kn-wl-item[aria-selected="true"] { font-weight: 600; }
|
||||||
|
.kn-wl[data-art="bereiche"] .kn-wl-ico svg { width: 21px; height: 21px; }
|
||||||
|
/* Reiter einer Unterseite: nur Wörter; passen sie nicht, setzt wasser-leiste.js data-eng */
|
||||||
|
.kn-wl[data-art="reiter"] .kn-wl-item { flex: 1 1 auto; justify-content: center; min-width: 0; padding: 0 9px; font-size: 13px; }
|
||||||
|
.kn-wl[data-art="reiter"][data-eng] .kn-wl-item { padding: 0 5px; font-size: 12px; letter-spacing: -.02em; }
|
||||||
|
.kn-wl-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
/* ---------- Einbau in die Hülle (Handy) ----------
|
/* ---------- Einbau in die Hülle (Handy) ----------
|
||||||
Die Pille schwebt: --kn-wl-luft Abstand zum unteren Rand (über dem Home-Balken des iPhones), darüber 10 px Luft zum Inhalt.
|
Die Pille schwebt mit --kn-wl-luft Abstand über dem unteren Rand: auf dem iPhone ganz über dem Wisch-Balken (Safe-Area plus 6 px), sonst 14 px.
|
||||||
--kn-wl-zone ist die Höhe dieser Zone; --kn-unten (Blatt, Chat) und der Abstand unter dem Inhalt rechnen damit statt mit der alten Zeile. */
|
Darüber 10 px Luft zum Inhalt. --kn-wl-zone ist die Höhe dieser Zone; --kn-unten (Blatt, Chat) und der Abstand unter dem Inhalt rechnen damit. */
|
||||||
|
|
||||||
.kn { --kn-wl-h: 54px; }
|
.kn { --kn-wl-h: 64px; }
|
||||||
|
|
||||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||||
.kn { --kn-zeile-h: calc(var(--kn-wl-h) + 10px); --kn-wl-luft: max(10px, calc(env(safe-area-inset-bottom) - 10px)); --kn-wl-zone: calc(var(--kn-zeile-h) + var(--kn-wl-luft)); --kn-unten: var(--kn-wl-zone); }
|
.kn { --kn-zeile-h: calc(var(--kn-wl-h) + 10px); --kn-wl-luft: max(14px, calc(env(safe-area-inset-bottom) + 6px)); --kn-wl-zone: calc(var(--kn-zeile-h) + var(--kn-wl-luft)); --kn-unten: var(--kn-wl-zone); }
|
||||||
.kn.kn-hat-streifen { --kn-unten: calc(var(--kn-wl-zone) + 44px); }
|
.kn.kn-hat-streifen { --kn-unten: calc(var(--kn-wl-zone) + 44px); }
|
||||||
.kn.kn-tastatur { --kn-unten: 0px; }
|
.kn.kn-tastatur { --kn-unten: 0px; }
|
||||||
|
|
||||||
.kn-haupt { padding-bottom: calc(var(--kn-wl-zone) + 28px); }
|
.kn-haupt { padding-bottom: calc(var(--kn-wl-zone) + 28px); }
|
||||||
.kn-hat-streifen .kn-haupt { padding-bottom: calc(var(--kn-wl-zone) + 28px + 44px); }
|
.kn-hat-streifen .kn-haupt { padding-bottom: calc(var(--kn-wl-zone) + 28px + 44px); }
|
||||||
.kn-streifen { bottom: var(--kn-wl-zone); }
|
.kn-streifen { bottom: var(--kn-wl-zone); }
|
||||||
/* Das Blatt reicht bis unten, sein Inhalt läuft unter der Pille durch */
|
|
||||||
.kn-blatt { bottom: 0; padding-bottom: calc(var(--kn-unten) + 24px); }
|
|
||||||
|
|
||||||
.kn-leiste { border-top: 0; background: none; padding-bottom: var(--kn-wl-luft); pointer-events: none; }
|
.kn-leiste { border-top: 0; background: none; padding-bottom: var(--kn-wl-luft); pointer-events: none; }
|
||||||
.kn-leiste-reel { position: relative; overflow: visible; }
|
.kn-leiste-reel { position: relative; overflow: visible; }
|
||||||
.kn-leiste .kn-rw {
|
.kn-leiste .kn-rw {
|
||||||
position: absolute; right: 12px; bottom: 0; left: 12px; width: fit-content; max-width: calc(100% - 24px); height: var(--kn-wl-h); margin: 0 auto; padding: 0;
|
position: absolute; right: 12px; bottom: 0; left: 12px; max-width: 480px; height: var(--kn-wl-h); margin: 0 auto; padding: 0; align-items: flex-end;
|
||||||
pointer-events: auto; transition: transform 450ms var(--kn-feder), opacity 250ms ease, visibility 0s;
|
pointer-events: none; transition: transform 450ms var(--kn-feder), opacity 250ms ease, visibility 0s;
|
||||||
}
|
}
|
||||||
|
.kn-leiste .kn-rw > * { pointer-events: auto; }
|
||||||
|
.kn-leiste .kn-wl { display: flex; width: 100%; }
|
||||||
/* huelle.css schiebt beide Zeilen eine Etage hoch; hier liegen die Pillen übereinander und tauschen mit kurzem Auf- und Abtauchen */
|
/* huelle.css schiebt beide Zeilen eine Etage hoch; hier liegen die Pillen übereinander und tauschen mit kurzem Auf- und Abtauchen */
|
||||||
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: none; }
|
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: none; }
|
||||||
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage {
|
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage {
|
||||||
opacity: 0; transform: translateY(18px) scale(.94); transition: transform 300ms var(--kn-bremse), opacity 200ms ease, visibility 0s 300ms;
|
opacity: 0; transform: translateY(18px) scale(.94); transition: transform 300ms var(--kn-bremse), opacity 200ms ease, visibility 0s 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Etage 2: gläserne Pille mit „Zurück“ und Reitern */
|
/* Unterseite (Aufbau in huelle.css): mit Unschärfe einblenden statt hochfahren; „Zurück“ oben rechts aus Glas */
|
||||||
#kn-zeile-etage {
|
.kn-blatt { filter: blur(14px); transition: opacity 380ms ease, filter 460ms var(--kn-bremse); }
|
||||||
min-width: 132px; padding: 5px; border-radius: 999px; isolation: isolate; background: rgba(255,255,255,.66);
|
.kn-blatt--auf { filter: none; }
|
||||||
-webkit-backdrop-filter: blur(12px) saturate(1.3) brightness(1.06); backdrop-filter: blur(12px) saturate(1.3) brightness(1.06);
|
.kn-blatt-zurueck { filter: blur(8px); transition: opacity 380ms ease, filter 460ms var(--kn-bremse), visibility 0s 460ms; }
|
||||||
box-shadow: inset 0 1px 1px rgba(255,255,255,.7), inset 0 0 0 1px rgba(255,255,255,.2), 0 12px 32px -14px rgba(0,40,90,.5), 0 2px 6px rgba(0,40,90,.1);
|
.kn-blatt--auf ~ .kn-blatt-zurueck { filter: none; }
|
||||||
}
|
.kn-blatt-zurueck .kn-zurueck { background: rgba(255,255,255,.62); -webkit-backdrop-filter: blur(12px) saturate(1.3) brightness(1.06); backdrop-filter: blur(12px) saturate(1.3) brightness(1.06); box-shadow: inset 0 1px 1px rgba(255,255,255,.7), inset 0 0 0 1px rgba(255,255,255,.2), 0 12px 32px -14px rgba(0,40,90,.5), 0 2px 6px rgba(0,40,90,.1); }
|
||||||
#kn-zeile-etage .kn-zurueck { height: 44px; padding: 0 14px 0 8px; border-radius: 999px; }
|
|
||||||
#kn-zeile-etage .kn-zurueck--voll { justify-content: center; }
|
|
||||||
#kn-zeile-etage .kn-balken { height: 44px; padding-right: 4px; border-radius: 999px; }
|
|
||||||
#kn-zeile-etage .kn-t { padding: 0 12px; }
|
|
||||||
#kn-zeile-etage .kn-gleiter {
|
|
||||||
z-index: 0; top: 4px; bottom: auto; height: 36px; border-radius: 999px; outline-offset: -3px;
|
|
||||||
background: radial-gradient(120% 140% at 30% 0%, rgba(255,255,255,.75), rgba(255,255,255,0) 55%), rgba(255,255,255,.38);
|
|
||||||
box-shadow: inset 0 1px 1px rgba(255,255,255,.95), inset 0 -2px 6px rgba(0,162,255,.22), inset 0 0 0 1px rgba(255,255,255,.55), 0 6px 16px -6px rgba(0,70,140,.45);
|
|
||||||
}
|
|
||||||
#kn-zeile-etage .kn-overlay { display: none; }
|
|
||||||
#kn-zeile-etage .kn-t[aria-selected="true"] { color: var(--kn-text); }
|
|
||||||
.kn #kn-zeile-etage .kn-balken:has(.kn-t:focus-visible) .kn-gleiter { outline-color: var(--kn-accent); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Querformat: niedrigere Pille */
|
/* Querformat: niedrigere Pille */
|
||||||
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
|
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
|
||||||
.kn { --kn-wl-h: 48px; }
|
.kn { --kn-wl-h: 54px; }
|
||||||
.kn .kn-wl-item { height: 36px; }
|
.kn-wl[data-art="bereiche"] .kn-wl-item { height: 42px; gap: 2px; }
|
||||||
#kn-zeile-etage { padding: 2px; }
|
.kn-wl[data-art="bereiche"] .kn-wl-ico svg { width: 18px; height: 18px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bewegung reduziert: keine Szenen, Pillen tauschen ohne Fahrt */
|
/* Bewegung reduziert: keine Szenen, Pillen tauschen ohne Fahrt */
|
||||||
@@ -136,4 +129,7 @@
|
|||||||
.kn-wl-ico svg, .kn-wl-ico::before, .kn-wl-ico::after, .kn-wl-tuer { animation: none !important; transition: none; }
|
.kn-wl-ico svg, .kn-wl-ico::before, .kn-wl-ico::after, .kn-wl-tuer { animation: none !important; transition: none; }
|
||||||
.kn-leiste .kn-rw,
|
.kn-leiste .kn-rw,
|
||||||
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { transform: none; transition: opacity 150ms, visibility 0s; }
|
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { transform: none; transition: opacity 150ms, visibility 0s; }
|
||||||
|
.kn-blatt { filter: none; transition: opacity 150ms; }
|
||||||
|
.kn-blatt-zurueck { filter: none; transition: opacity 150ms, visibility 0s 150ms; }
|
||||||
|
.kn-blatt--auf ~ .kn-blatt-zurueck { filter: none; transition: opacity 150ms; }
|
||||||
}
|
}
|
||||||
+23
-12
@@ -3,7 +3,8 @@
|
|||||||
Bereich, reißt bei langen Sprüngen in Tröpfchen und fließt mit einer Welle wieder zusammen; jeder Tipp schickt eine Welle durch die Pille, jedes
|
Bereich, reißt bei langen Sprüngen in Tröpfchen und fließt mit einer Welle wieder zusammen; jeder Tipp schickt eine Welle durch die Pille, jedes
|
||||||
Symbol spielt eine kleine Szene. Das Wasser zeichnet ein kleiner WebGL-Shader (nur in der Pille). Er ruht, solange die Pille nicht zu sehen ist,
|
Symbol spielt eine kleine Szene. Das Wasser zeichnet ein kleiner WebGL-Shader (nur in der Pille). Er ruht, solange die Pille nicht zu sehen ist,
|
||||||
und kommt einige Sekunden nach der letzten Bedienung zur Ruhe (Akku). Ohne WebGL zeigt die Pille einen CSS-Tropfen (wasser-leiste.css).
|
und kommt einige Sekunden nach der letzten Bedienung zur Ruhe (Akku). Ohne WebGL zeigt die Pille einen CSS-Tropfen (wasser-leiste.css).
|
||||||
Rollen wie die Fahrstuhl-Zeile davor: tablist mit tab, aria-selected und aria-current; Zähler als .kn-zahl am Symbol, Beschriftung per aria-label.
|
Zwei Arten: 'bereiche' (Hauptleiste: Symbol über dem Wort, Zähler als .kn-zahl am Symbol) und 'reiter' (Reiter einer Unterseite: nur Wörter,
|
||||||
|
Kurzwort `kurz` falls vorhanden). Rollen wie die Fahrstuhl-Zeile davor: tablist mit tab, aria-selected und aria-current, Beschriftung per aria-label.
|
||||||
Die Rechenfunktionen (Feder, Tröpfchen, Wellen) sind rein und werden mit node --test geprüft; ohne Browser baut das Modul nur die Knöpfe. */
|
Die Rechenfunktionen (Feder, Tröpfchen, Wellen) sind rein und werden mit node --test geprüft; ohne Browser baut das Modul nur die Knöpfe. */
|
||||||
|
|
||||||
import { h } from './dom.js'
|
import { h } from './dom.js'
|
||||||
@@ -291,15 +292,17 @@ const beschriftung = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wasser-Leiste bauen. Eintrag { key, label, symbol?, fx?, einheit?, ausrufText? }; symbol und fx folgen ohne Angabe dem key (SYMBOLE, SZENEN).
|
* Wasser-Leiste bauen. Eintrag { key, label, kurz?, symbol?, fx?, einheit?, ausrufText? }. Art 'bereiche': symbol und fx folgen ohne Angabe dem key
|
||||||
* bei(key) wird bei jedem Tipp gerufen, auch auf den aktiven Bereich (die Hülle scrollt dann nach oben).
|
* (SYMBOLE, SZENEN). Art 'reiter': kein Symbol, keine Szene, sichtbar ist `kurz` (sonst label), vorgelesen label.
|
||||||
* Liefert das Element mit setzeAktiv(key), setzeAnzeige(key, { zahl, ausruf }), messen() und beende().
|
* bei(key) wird bei jedem Tipp gerufen, auch auf den aktiven Eintrag (die Hülle scrollt dann nach oben).
|
||||||
|
* Liefert das Element mit setzeAktiv(key), setzeAnzeige(key, { zahl, ausruf }), messen() und beende() (gibt auch den WebGL-Kontext frei).
|
||||||
*/
|
*/
|
||||||
export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche', bei = () => {}, warte = (fn, ms) => setTimeout(fn, ms) }) {
|
export function baueWasserLeiste({ eintraege, aktiv = '', art = 'bereiche', ariaLabel = 'Bereiche', bei = () => {}, warte = (fn, ms) => setTimeout(fn, ms) }) {
|
||||||
const win = globalThis.window
|
const win = globalThis.window
|
||||||
const doc = globalThis.document
|
const doc = globalThis.document
|
||||||
const s = neuerZustand()
|
const s = neuerZustand()
|
||||||
const liste = eintraege.map((e) => ({ ...e, fx: e.fx ?? SZENEN[e.key] ?? 'pop', zahl: 0, ausruf: false }))
|
const bereiche = art === 'bereiche'
|
||||||
|
const liste = eintraege.map((e) => ({ ...e, fx: e.fx ?? (bereiche ? SZENEN[e.key] ?? 'pop' : 'none'), zahl: 0, ausruf: false }))
|
||||||
let aktuell = aktiv
|
let aktuell = aktiv
|
||||||
let glc = null
|
let glc = null
|
||||||
let glVersucht = false // WebGL erst, wenn die Pille zu sehen ist (am Desktop nie)
|
let glVersucht = false // WebGL erst, wenn die Pille zu sehen ist (am Desktop nie)
|
||||||
@@ -308,15 +311,15 @@ export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche'
|
|||||||
const jetzt = () => (globalThis.performance?.now ? globalThis.performance.now() : Date.now())
|
const jetzt = () => (globalThis.performance?.now ? globalThis.performance.now() : Date.now())
|
||||||
|
|
||||||
for (const e of liste) {
|
for (const e of liste) {
|
||||||
e.ico = h('span', { class: 'kn-wl-ico' }, baueSymbol(e.symbol ?? e.key, doc))
|
e.ico = bereiche ? h('span', { class: 'kn-wl-ico' }, baueSymbol(e.symbol ?? e.key, doc)) : null
|
||||||
e.tab = h('button', { class: 'kn-wl-item', role: 'tab', type: 'button', 'data-key': e.key, 'aria-selected': 'false', 'aria-label': e.label, tabindex: '-1', on: { click: () => waehle(e.key) } },
|
e.tab = h('button', { class: 'kn-wl-item', role: 'tab', type: 'button', 'data-key': e.key, 'aria-selected': 'false', 'aria-label': e.label, tabindex: '-1', on: { click: () => waehle(e.key) } },
|
||||||
e.ico, h('span', { class: 'kn-wl-label' }, e.label))
|
e.ico, h('span', { class: 'kn-wl-label' }, e.kurz ?? e.label))
|
||||||
}
|
}
|
||||||
const tabs = liste.map((e) => e.tab)
|
const tabs = liste.map((e) => e.tab)
|
||||||
const canvas = h('canvas', { class: 'kn-wl-wasser', 'aria-hidden': 'true' })
|
const canvas = h('canvas', { class: 'kn-wl-wasser', 'aria-hidden': 'true' })
|
||||||
const tropfen = h('span', { class: 'kn-wl-tropfen', 'aria-hidden': 'true' })
|
const tropfen = h('span', { class: 'kn-wl-tropfen', 'aria-hidden': 'true' })
|
||||||
const wurzel = h('div', {
|
const wurzel = h('div', {
|
||||||
class: 'kn-wl', role: 'tablist', 'aria-label': ariaLabel, 'data-gl': 'on', 'data-brechung': 'aus',
|
class: 'kn-wl', role: 'tablist', 'aria-label': ariaLabel, 'data-art': bereiche ? 'bereiche' : 'reiter', 'data-gl': 'on', 'data-brechung': 'aus',
|
||||||
on: { keydown: beiTaste, pointermove: beiMaus, pointerleave: () => { s.maus.on = false; schleife() }, pointerdown: beiDruck },
|
on: { keydown: beiTaste, pointermove: beiMaus, pointerleave: () => { s.maus.on = false; schleife() }, pointerdown: beiDruck },
|
||||||
}, baueFilter(doc), h('span', { class: 'kn-wl-glas', 'aria-hidden': 'true' }), canvas, tropfen, tabs)
|
}, baueFilter(doc), h('span', { class: 'kn-wl-glas', 'aria-hidden': 'true' }), canvas, tropfen, tabs)
|
||||||
|
|
||||||
@@ -370,6 +373,10 @@ export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche'
|
|||||||
function messen() {
|
function messen() {
|
||||||
if (!(wurzel.offsetWidth > 0)) return // nicht zu sehen (Desktop, Tastatur offen) oder ohne Browser
|
if (!(wurzel.offsetWidth > 0)) return // nicht zu sehen (Desktop, Tastatur offen) oder ohne Browser
|
||||||
if (!glVersucht) starteGL()
|
if (!glVersucht) starteGL()
|
||||||
|
if (!bereiche) { // passen die Wörter nicht, wird die Schrift kleiner (wie schriftStufe in reiter.js)
|
||||||
|
wurzel.removeAttribute('data-eng')
|
||||||
|
if (wurzel.scrollWidth > wurzel.clientWidth + 1) wurzel.setAttribute('data-eng', '')
|
||||||
|
}
|
||||||
s.w = wurzel.offsetWidth
|
s.w = wurzel.offsetWidth
|
||||||
s.h = wurzel.offsetHeight
|
s.h = wurzel.offsetHeight
|
||||||
const el = liste.find((e) => e.key === aktuell)?.tab
|
const el = liste.find((e) => e.key === aktuell)?.tab
|
||||||
@@ -398,13 +405,14 @@ export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche'
|
|||||||
wecke()
|
wecke()
|
||||||
}
|
}
|
||||||
function symbolMitte(i) {
|
function symbolMitte(i) {
|
||||||
const b = liste[i]?.ico?.getBoundingClientRect?.()
|
const b = (liste[i]?.ico ?? liste[i]?.tab)?.getBoundingClientRect?.()
|
||||||
return b ? lokal(b.left + b.width / 2, b.top + b.height / 2) : null
|
return b ? lokal(b.left + b.width / 2, b.top + b.height / 2) : null
|
||||||
}
|
}
|
||||||
/** Szene des Symbols (neu) starten; die anderen Symbole kehren in die Ruhe zurück. */
|
/** Szene des Symbols (neu) starten; die anderen Symbole kehren in die Ruhe zurück. */
|
||||||
function spiele(i) {
|
function spiele(i) {
|
||||||
liste.forEach((e, j) => { if (j !== i) e.ico.removeAttribute('data-fx') })
|
liste.forEach((e, j) => { if (j !== i) e.ico?.removeAttribute('data-fx') })
|
||||||
const e = liste[i]
|
const e = liste[i]
|
||||||
|
if (!e.ico) return
|
||||||
e.ico.removeAttribute('data-fx')
|
e.ico.removeAttribute('data-fx')
|
||||||
if (e.fx === 'none') return
|
if (e.fx === 'none') return
|
||||||
void e.ico.offsetWidth // Stil auflösen, damit die Animation von vorn beginnt
|
void e.ico.offsetWidth // Stil auflösen, damit die Animation von vorn beginnt
|
||||||
@@ -467,7 +475,7 @@ export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche'
|
|||||||
|
|
||||||
function setzeAnzeige(key, { zahl = 0, ausruf = false } = {}) {
|
function setzeAnzeige(key, { zahl = 0, ausruf = false } = {}) {
|
||||||
const e = liste.find((x) => x.key === key)
|
const e = liste.find((x) => x.key === key)
|
||||||
if (!e) return
|
if (!e?.ico) return
|
||||||
e.zahl = zahl
|
e.zahl = zahl
|
||||||
e.ausruf = ausruf
|
e.ausruf = ausruf
|
||||||
const text = anzeigeText({ zahl, ausruf })
|
const text = anzeigeText({ zahl, ausruf })
|
||||||
@@ -522,6 +530,9 @@ export function baueWasserLeiste({ eintraege, aktiv = '', ariaLabel = 'Bereiche'
|
|||||||
for (const fn of aufraeumen.splice(0)) fn()
|
for (const fn of aufraeumen.splice(0)) fn()
|
||||||
if (raf) win?.cancelAnimationFrame?.(raf)
|
if (raf) win?.cancelAnimationFrame?.(raf)
|
||||||
raf = 0
|
raf = 0
|
||||||
|
glc?.gl.getExtension('WEBGL_lose_context')?.loseContext() // Browser halten nur wenige Kontexte: jede Unterseite baut ihre Reiter neu
|
||||||
|
glc = null
|
||||||
|
glVersucht = true
|
||||||
}
|
}
|
||||||
|
|
||||||
wurzel.setzeAktiv = setzeAktiv
|
wurzel.setzeAktiv = setzeAktiv
|
||||||
|
|||||||
@@ -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-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-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-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.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')
|
assert.doesNotMatch(HUELLE, /\.kn-banner:not\(\[hidden\]\) ~ \.kn-huelle \.kn-menue/, 'die feste Höhe 36 px ist weg')
|
||||||
const js = quelle('huelle.js')
|
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 { baueEtage } from '../public/neu/etage.js'
|
||||||
import { lies, handyTeil } from './support/knHuelle.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 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' }
|
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()
|
const fx = installFakeDom()
|
||||||
try {
|
try {
|
||||||
const t = teile(); let zu = 0; const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
const t = teile(); let zu = 0; const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
||||||
e.oeffne({ titel: 'Terminplaner', eltern: EL, seite: 'termine' })
|
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([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'])
|
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)
|
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() }
|
} 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()
|
const fx = installFakeDom()
|
||||||
try {
|
try {
|
||||||
const gew = []; let handy = true; const t = teile(); const e = baueEtage({ ...t, istHandy: () => handy, onZurueck() {} })
|
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) } })
|
await fx.sende(kind, 'kn:reiter', { detail: { eintraege: REITER, aktiv: 'r0', bei: (k) => gew.push(k) } })
|
||||||
const tabs = fx.alle(t.zeile, '[role=tab]')
|
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(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.klick(tabs[2]); assert.deepEqual(gew, ['r2'])
|
||||||
await fx.sende(kind, 'kn:reiter', { detail: { eintraege: [], aktiv: '', bei() {} } })
|
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() {} } })
|
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])
|
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')
|
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 {
|
try {
|
||||||
const t = teile(); const e = baueEtage({ ...t, istHandy: () => true, onZurueck() {} })
|
const t = teile(); const e = baueEtage({ ...t, istHandy: () => true, onZurueck() {} })
|
||||||
e.oeffne({ titel: 'Webshop', eltern: EL, seite: 'webshop' }); e.setzeChatPunkt(true)
|
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() }
|
} finally { fx.restore() }
|
||||||
})
|
})
|
||||||
|
|
||||||
test('huelle.css (Blatt): Handy fährt von unten ein, Zurück füllt die Zeile', () => {
|
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 css = lies('huelle.css'); const handy = handyTeil(css); const wl = handyTeil(lies('wasser-leiste.css'))
|
||||||
assert.match(handyTeil(css), /\.kn-blatt\s*\{(?=[^}]*border-radius:\s*12px 12px 0 0)(?=[^}]*transition:[^}]*550ms[^}]*var\(--kn-bremse\))/s)
|
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.match(css, /\.kn-zurueck--voll\s*\{[^}]*flex:\s*1/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 () => {
|
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 t = teile(); t.blatt.append(h('section', { 'data-etage': 'webshop' })); let zu = 0
|
||||||
const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
const e = baueEtage({ ...t, istHandy: () => true, onZurueck: () => { zu += 1 } })
|
||||||
e.setzeChatPunkt(true) // vor dem Öffnen gemerkt
|
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
|
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'))
|
await fx.klick(fx.finde(t.brot, 'a'))
|
||||||
assert.equal(zu, 1) // Klick auf die Brotkrume: wie Zurück, ohne Fragment-Navigation
|
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() {} } })
|
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
|
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' })
|
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() {} } })
|
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
|
assert.equal(fx.alle(t.zeile, '[role=tab]').length, 0) // geschlossen: späte Meldungen tun nichts
|
||||||
} finally { fx.restore() }
|
} finally { fx.restore() }
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ test('Etage Terminplaner: wunschProjekt aus ?projekt=, Modul auf #termine-body m
|
|||||||
const seen = []
|
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]) }) })
|
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(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') } })
|
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.')
|
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() }
|
} 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', () => {
|
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) }
|
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)
|
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)
|
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-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, /\.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:`))
|
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.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.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.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-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-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-zeile-etage\s*\{[^}]*border-radius:\s*999px[^}]*backdrop-filter/s)
|
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)
|
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)
|
// 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\)/)
|
assert.match(lies('huelle.css'), /\.kn-leiste\s*\{[^}]*border-top:\s*1px solid var\(--kn-line\)/)
|
||||||
|
|||||||
Reference in new issue
Block a user