Navigation leichter: Handy-Leiste, Schwarz nur für die Hauptaktion, Verträge mit zwei Reitern, kürzeres Konto (V3, V5, V7, V8)
V3: In der Fahrstuhl-Zeile läuft unter dem aktiven Wort eine dünne Linie statt des schwarzen Blocks; das Wort wird dunkel und fett. Die weiße Overlay-Kopie bleibt im Baum, wird aber nicht mehr gezeigt. V5: Terminplaner: Tag/Woche/Monat als hellgrauer Umschalter mit weißer Auswahl, Filter als leise Chips. „Einschalten“ in der Liste Entdecken ist ein heller Knopf; schwarz bleibt der Knopf der Bestätigungsseite. V7: Verträge nur noch mit Abo und Rechnungen. Das Stundenkonto steht beim Wartungs-Abo als Abschnitt unter dem Abo (ohne doppelte Warnungen), #stunden springt dorthin. Der E-Mail-Speicher ist die Unterseite #speicher (Link „ändern“ in der Abo-Zeile), ohne Widerrufsstreifen wie die anderen Bestellseiten. Alte Links bleiben gültig. V8: Konto mit vier Zeilen (Passwort, Verträge kündigen, Vertrag widerrufen, Abmelden), Datenschutz und Impressum klein darunter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a270ff51da
commit
83ac903ab3
18 files changed
+152
-96
No files matched your search
@@ -283,7 +283,7 @@ export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {})
|
|||||||
let wahl = null
|
let wahl = null
|
||||||
const knopf = h('button', {
|
const knopf = h('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: 'kn-fn-knopf',
|
class: 'kn-fn-knopf kn-fn-knopf--zweit', // Navigation leichter V5: jede Karte hat ihn, keiner ist die eine Hauptaktion; schwarz bleibt der Knopf der Bestätigungsseite
|
||||||
'data-aktion': 'einschalten',
|
'data-aktion': 'einschalten',
|
||||||
on: {
|
on: {
|
||||||
click: () => {
|
click: () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Kundenansicht neu (P7): Reiter E-Mail in Verträge, der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
/* Kundenansicht neu (P7): Unterseite E-Mail-Speicher der Verträge (#speicher, bis „Navigation leichter“ V7 der Reiter E-Mail), der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
||||||
Plus: Block „Was du bestellst“, eine Linkzeile zu den Regeln, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die
|
Plus: Block „Was du bestellst“, eine Linkzeile zu den Regeln, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die
|
||||||
Kundenart-Zeile; kein Häkchen (der Klick auf den Knopf ist die Zustimmung). Minus: genau eine Warnzeile und der Knopf „Speichern“.
|
Kundenart-Zeile; kein Häkchen (der Klick auf den Knopf ist die Zustimmung). Minus: genau eine Warnzeile und der Knopf „Speichern“.
|
||||||
Die Sätze kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst, der Client sendet nie kundenart. */
|
Die Sätze kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst, der Client sendet nie kundenart. */
|
||||||
@@ -84,8 +84,7 @@ function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) {
|
|||||||
const status = []
|
const status = []
|
||||||
if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Dein zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.'))
|
if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Dein zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.'))
|
||||||
if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`))
|
if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`))
|
||||||
ersetze(panel,
|
ersetze(panel, // die Überschrift „E-Mail-Speicher“ trägt die Unterseite selbst
|
||||||
h('h3', null, 'E-Mail-Speicher'),
|
|
||||||
h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`),
|
h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`),
|
||||||
notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
|
notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
|
||||||
!canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null,
|
!canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null,
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ function gutschriftZeile(c) {
|
|||||||
h('td', { 'data-label': 'Zeit', class: 'kn-num' }, `+${fmtStd(c.minutes)}`, h('span', { class: 'kn-zeile-sub' }, ` ${c.expired ? 'abgelaufen' : `nutzbar bis ${fmtDatum(c.expiresAt)}`}`)))
|
h('td', { 'data-label': 'Zeit', class: 'kn-num' }, `+${fmtStd(c.minutes)}`, h('span', { class: 'kn-zeile-sub' }, ` ${c.expired ? 'abgelaufen' : `nutzbar bis ${fmtDatum(c.expiresAt)}`}`)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reiter-Inhalt Stunden. daten (GET /api/abo) ist optional; ohne sie lädt die Funktion selbst. */
|
/**
|
||||||
export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
* Stundenkonto. daten (GET /api/abo) ist optional; ohne sie lädt die Funktion selbst.
|
||||||
|
* eingebettet: Abschnitt unter dem Abo (Navigation leichter V7). Dann ohne die Warnungen und den Bedingungen-Link, die das Abo darüber schon zeigt,
|
||||||
|
* und ohne Hinweis, wenn es kein Wartungs-Abo gibt (der Abschnitt steht dann gar nicht da).
|
||||||
|
*/
|
||||||
|
export async function zeigeStunden(wurzel, { route, me, nav, daten, eingebettet = false } = {}) {
|
||||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||||
laeufe.set(wurzel, nr)
|
laeufe.set(wurzel, nr)
|
||||||
const aktuell = () => laeufe.get(wurzel) === nr
|
const aktuell = () => laeufe.get(wurzel) === nr
|
||||||
@@ -46,7 +50,8 @@ export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
|||||||
const sub = abo.subscription
|
const sub = abo.subscription
|
||||||
const l = abo.ledger
|
const l = abo.ledger
|
||||||
if (!sub?.active || sub.productKey !== 'wartung' || !l) {
|
if (!sub?.active || sub.productKey !== 'wartung' || !l) {
|
||||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
leeren(wurzel)
|
||||||
|
if (!eingebettet) wurzel.append(h('p', { class: K.leer }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
|
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
|
||||||
@@ -54,16 +59,16 @@ export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
|||||||
const zeilen = [...(l.usage || []).map(verlaufZeile), ...(l.credits || []).slice().reverse().map(gutschriftZeile)]
|
const zeilen = [...(l.usage || []).map(verlaufZeile), ...(l.credits || []).slice().reverse().map(gutschriftZeile)]
|
||||||
|
|
||||||
ersetze(wurzel,
|
ersetze(wurzel,
|
||||||
...aboWarnungen(sub),
|
...(eingebettet ? [] : aboWarnungen(sub)),
|
||||||
h('div', { class: 'kn-statistik' },
|
h('div', { class: 'kn-statistik' },
|
||||||
stat(fmtStd(l.balanceMinutes), 'verfügbare Zeit'),
|
stat(fmtStd(l.balanceMinutes), 'verfügbare Zeit'),
|
||||||
stat(fmtStd(l.usedMinutes), 'bisher genutzt'),
|
stat(fmtStd(l.usedMinutes), 'bisher genutzt'),
|
||||||
l.nextExpiry ? stat(fmtStd(l.nextExpiry.minutes), `verfallen am ${fmtDatum(l.nextExpiry.at)}`) : null),
|
l.nextExpiry ? stat(fmtStd(l.nextExpiry.minutes), `verfallen am ${fmtDatum(l.nextExpiry.at)}`) : null),
|
||||||
vorschuss ? h('p', { class: 'kn-hinweis kn-vorschuss' }, vorschuss) : null,
|
vorschuss ? h('p', { class: 'kn-hinweis kn-vorschuss' }, vorschuss) : null,
|
||||||
h('p', { class: 'kn-hinweis' }, '1 Stunde pro bezahltem Monat. Angerechnet wird in 15-Minuten-Schritten, die ältesten Stunden zuerst.'),
|
h('p', { class: 'kn-hinweis' }, '1 Stunde pro bezahltem Monat. Angerechnet wird in 15-Minuten-Schritten, die ältesten Stunden zuerst.'),
|
||||||
h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Leistungen & Bedingungen')),
|
eingebettet ? null : h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Leistungen & Bedingungen')),
|
||||||
h('section', { class: 'kn-sektion' },
|
h('section', { class: 'kn-sektion' },
|
||||||
h('h2', { class: K.abschnitt }, 'Verlauf'),
|
h(eingebettet ? 'h3' : 'h2', { class: K.abschnitt }, 'Verlauf'),
|
||||||
zeilen.length
|
zeilen.length
|
||||||
? h('div', { class: 'kn-tabelle' }, h('table', null, h('thead', null, h('tr', null, h('th', null, 'Datum'), h('th', null, 'Vorgang'), h('th', null, 'Zeit'))), h('tbody', null, zeilen)))
|
? h('div', { class: 'kn-tabelle' }, h('table', null, h('thead', null, h('tr', null, h('th', null, 'Datum'), h('th', null, 'Vorgang'), h('th', null, 'Zeit'))), h('tbody', null, zeilen)))
|
||||||
: h('p', { class: 'kn-hinweis' }, 'Noch keine Buchungen.')))
|
: h('p', { class: 'kn-hinweis' }, 'Noch keine Buchungen.')))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Rechnungen, Abo, Stunden und E-Mail. Der Rahmen lädt Abo und Rechnungen
|
/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Abo und Rechnungen (Navigation leichter V7, vorher dazu Stunden und E-Mail).
|
||||||
parallel (jede Quelle mit eigenem Fehlerweg), wählt den Reiter allein aus route.reiter und gibt die schon geladenen Antworten an
|
Das Stundenkonto des Wartungs-Abos steht als Abschnitt unter dem Abo; #stunden öffnet das Abo und springt dorthin. Der E-Mail-Speicher ist
|
||||||
die Reiter-Inhalte weiter. Bestellen, Wechsel und Gutschrift sind Etagen, die die Hülle direkt ruft. */
|
die Etage #speicher (Link „ändern“ im Abo). Der Rahmen lädt Abo und Rechnungen parallel (jede Quelle mit eigenem Fehlerweg), wählt den
|
||||||
|
Reiter allein aus route.reiter und gibt die schon geladenen Antworten an die Reiter-Inhalte weiter. Bestellen, Wechsel, Gutschrift und
|
||||||
|
Speicher sind Etagen, die die Hülle direkt ruft. */
|
||||||
|
|
||||||
import { api } from './api.js'
|
import { api } from './api.js'
|
||||||
import { h, leeren } from './dom.js'
|
import { h, leeren } from './dom.js'
|
||||||
@@ -11,26 +13,24 @@ import { offeneRechnungen } from './rechnung-offen.js'
|
|||||||
import { zeigeRechnungen } from './vertraege-rechnungen.js'
|
import { zeigeRechnungen } from './vertraege-rechnungen.js'
|
||||||
import { zeigeAbo } from './vertraege-abo.js'
|
import { zeigeAbo } from './vertraege-abo.js'
|
||||||
import { zeigeStunden } from './ansicht-stunden.js'
|
import { zeigeStunden } from './ansicht-stunden.js'
|
||||||
import { zeigeSpeicher } from './ansicht-speicher.js'
|
|
||||||
|
|
||||||
/** Nach ?abo=success: so oft und in diesem Abstand wird auf ein laufendes Abo gewartet (Tests verkürzen es). */
|
/** Nach ?abo=success: so oft und in diesem Abstand wird auf ein laufendes Abo gewartet (Tests verkürzen es). */
|
||||||
export const AUF_ABO_WARTEN = { versuche: 10, pauseMs: 2000 }
|
export const AUF_ABO_WARTEN = { versuche: 10, pauseMs: 2000 }
|
||||||
|
|
||||||
const LABEL = { rechnungen: 'Rechnungen', abo: 'Abo', stunden: 'Stunden', email: 'E-Mail' }
|
const LABEL = { abo: 'Abo', rechnungen: 'Rechnungen' }
|
||||||
// Reiterklicks navigieren mit den ausdrücklichen Hashes (P1 schreibt #wartung und #abo kanonisch um)
|
// Reiterklicks navigieren mit den ausdrücklichen Hashes (P1 schreibt #wartung und #abo kanonisch um)
|
||||||
const HASH = { rechnungen: '#rechnungen', abo: '#abo', stunden: '#stunden', email: '#speicher' }
|
const HASH = { abo: '#abo', rechnungen: '#rechnungen' }
|
||||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||||
let reiterWechsel = null // Reiterklick mit laufendem Router: der nächste Aufruf nimmt die schon geladenen Antworten
|
let reiterWechsel = null // Reiterklick mit laufendem Router: der nächste Aufruf nimmt die schon geladenen Antworten
|
||||||
|
|
||||||
/** Sichtbare Reiter: ohne laufendes Abo Rechnungen und Abo; Hosting dazu E-Mail; Wartung dazu Stunden und E-Mail. */
|
/** Sichtbare Reiter: immer Abo und Rechnungen (Navigation leichter V7). Stunden stehen im Abo, der E-Mail-Speicher ist die Etage #speicher. */
|
||||||
export function sichtbareReiter(daten) {
|
export function sichtbareReiter() {
|
||||||
const sub = daten?.subscription
|
return ['abo', 'rechnungen']
|
||||||
const reiter = ['rechnungen', 'abo']
|
|
||||||
if (!sub?.active) return reiter
|
|
||||||
if (sub.productKey === 'hosting') return [...reiter, 'email']
|
|
||||||
return [...reiter, 'stunden', 'email']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gibt es ein Stundenkonto unter dem Abo? Nur mit laufendem Wartungs-Abo. */
|
||||||
|
export const mitStundenkonto = (daten) => Boolean(daten?.subscription?.active && daten.subscription.productKey === 'wartung')
|
||||||
|
|
||||||
/** Reiterwahl allein aus route.reiter (routeReiter) und den Daten, nie aus der Adressleiste. Nur der Bereichs-Einstieg (ohne Reiter) springt auf Rechnungen. */
|
/** Reiterwahl allein aus route.reiter (routeReiter) und den Daten, nie aus der Adressleiste. Nur der Bereichs-Einstieg (ohne Reiter) springt auf Rechnungen. */
|
||||||
export function waehleReiter({ routeReiter, sichtbar, offeneRechnungen: offen }) {
|
export function waehleReiter({ routeReiter, sichtbar, offeneRechnungen: offen }) {
|
||||||
if (routeReiter) return sichtbar.includes(routeReiter) ? routeReiter : 'abo'
|
if (routeReiter) return sichtbar.includes(routeReiter) ? routeReiter : 'abo'
|
||||||
@@ -79,7 +79,8 @@ export async function zeigeVertraege(wurzel, { route, me, nav } = {}) {
|
|||||||
const sichtbar = sichtbareReiter(daten)
|
const sichtbar = sichtbareReiter(daten)
|
||||||
const offen = offeneRechnungen(rechnungen).length
|
const offen = offeneRechnungen(rechnungen).length
|
||||||
const gewaehlt = waehleReiter({ routeReiter: route?.reiter, sichtbar, offeneRechnungen: offen })
|
const gewaehlt = waehleReiter({ routeReiter: route?.reiter, sichtbar, offeneRechnungen: offen })
|
||||||
const stundenOhneWartung = route?.reiter === 'stunden' && !sichtbar.includes('stunden')
|
const stundenGewuenscht = route?.reiter === 'stunden' // #stunden: Abo zeigen und zum Stundenkonto springen
|
||||||
|
const stundenkonto = mitStundenkonto(daten)
|
||||||
const hinweis = nimmHinweis()
|
const hinweis = nimmHinweis()
|
||||||
|
|
||||||
const inhalt = h('div', { class: 'kn-reiter-inhalt' })
|
const inhalt = h('div', { class: 'kn-reiter-inhalt' })
|
||||||
@@ -87,14 +88,20 @@ export async function zeigeVertraege(wurzel, { route, me, nav } = {}) {
|
|||||||
leeren(inhalt)
|
leeren(inhalt)
|
||||||
if (key === 'rechnungen') return zeigeRechnungen(inhalt, { me, antwort: rechnungen ?? undefined })
|
if (key === 'rechnungen') return zeigeRechnungen(inhalt, { me, antwort: rechnungen ?? undefined })
|
||||||
if (!daten) return Promise.resolve()
|
if (!daten) return Promise.resolve()
|
||||||
if (key === 'abo') {
|
if (stundenGewuenscht && !stundenkonto) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||||
if (stundenOhneWartung) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
const platz = h('div', { class: 'kn-reiter-abo' })
|
||||||
const platz = h('div', { class: 'kn-reiter-abo' })
|
inhalt.append(platz)
|
||||||
inhalt.append(platz)
|
const fertig = [zeigeAbo(platz, { daten, me })]
|
||||||
return zeigeAbo(platz, { daten, me })
|
if (stundenkonto) {
|
||||||
|
const konto = h('div')
|
||||||
|
const abschnitt = h('section', { class: 'kn-sektion kn-stundenkonto', id: 'kn-stundenkonto', 'aria-labelledby': 'kn-stundenkonto-titel' },
|
||||||
|
h('h2', { class: K.abschnitt, id: 'kn-stundenkonto-titel' }, 'Stundenkonto'), konto)
|
||||||
|
inhalt.append(abschnitt)
|
||||||
|
fertig.push(zeigeStunden(konto, { route, me, nav, daten, eingebettet: true }).then(() => {
|
||||||
|
if (stundenGewuenscht && nav !== 'back' && typeof abschnitt.scrollIntoView === 'function') abschnitt.scrollIntoView({ block: 'start' })
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
if (key === 'stunden') return zeigeStunden(inhalt, { route, me, nav, daten })
|
return Promise.all(fertig)
|
||||||
return zeigeSpeicher(inhalt, { route, me, nav, daten })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bei = (key) => {
|
const bei = (key) => {
|
||||||
|
|||||||
@@ -423,3 +423,6 @@
|
|||||||
.kn-rechnungen .kn-rechnung-aktionen { justify-content: flex-start; padding-top: 6px; }
|
.kn-rechnungen .kn-rechnung-aktionen { justify-content: flex-start; padding-top: 6px; }
|
||||||
.kn-rechnungen .kn-rechnung-aktionen::before { content: none; }
|
.kn-rechnungen .kn-rechnung-aktionen::before { content: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Navigation leichter (V7): Stundenkonto als Abschnitt unter dem Abo; #stunden springt hierher, der Kopf deckt die Überschrift nicht zu */
|
||||||
|
.kn-stundenkonto { scroll-margin-top: calc(var(--kn-kopf-h, 52px) + 16px); }
|
||||||
+13
-14
@@ -64,7 +64,10 @@
|
|||||||
.kn-konto-zeile { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 46px; padding: 0; border-bottom: 1px solid var(--kn-line); text-align: left; color: var(--kn-text); }
|
.kn-konto-zeile { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 46px; padding: 0; border-bottom: 1px solid var(--kn-line); text-align: left; color: var(--kn-text); }
|
||||||
.kn-konto-zeile:hover .kn-konto-text { text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
.kn-konto-zeile:hover .kn-konto-text { text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||||
.kn-konto-zeile--betont .kn-konto-text { font-weight: 600; }
|
.kn-konto-zeile--betont .kn-konto-text { font-weight: 600; }
|
||||||
.kn-konto-zeile[data-konto="abmelden"] { border-bottom: 0; color: var(--kn-text-2); }
|
.kn-konto-zeile[data-konto="abmelden"] { color: var(--kn-text-2); }
|
||||||
|
/* Navigation leichter (V8): Datenschutz und Impressum klein nebeneinander unter den vier Zeilen */
|
||||||
|
.kn-konto-liste { display: flex; flex-wrap: wrap; column-gap: 16px; }
|
||||||
|
.kn-konto-zeile--klein { width: auto; min-height: 36px; border-bottom: 0; font-size: 13px; line-height: 18px; color: var(--kn-text-2); }
|
||||||
.kn-konto-text { display: block; }
|
.kn-konto-text { display: block; }
|
||||||
.kn-konto-hinweis { display: block; margin-top: 1px; font-size: 13px; line-height: 18px; font-weight: 400; color: var(--kn-text-2); }
|
.kn-konto-hinweis { display: block; margin-top: 1px; font-size: 13px; line-height: 18px; font-weight: 400; color: var(--kn-text-2); }
|
||||||
|
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
.kn-konto { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 92vh; padding: 8px max(20px, env(safe-area-inset-right)) calc(22px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); border-radius: 12px 12px 0 0; animation: kn-sheet-hoch 500ms var(--kn-bremse); }
|
.kn-konto { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 92vh; padding: 8px max(20px, env(safe-area-inset-right)) calc(22px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); border-radius: 12px 12px 0 0; animation: kn-sheet-hoch 500ms var(--kn-bremse); }
|
||||||
.kn-konto-zu { min-height: 44px; }
|
.kn-konto-zu { min-height: 44px; }
|
||||||
.kn-konto-zeile { min-height: 46px; }
|
.kn-konto-zeile { min-height: 46px; }
|
||||||
|
.kn-konto-zeile--klein { min-height: 44px; }
|
||||||
}
|
}
|
||||||
@keyframes kn-sheet-hoch { from { transform: translateY(100%); } }
|
@keyframes kn-sheet-hoch { from { transform: translateY(100%); } }
|
||||||
|
|
||||||
@@ -133,7 +137,9 @@
|
|||||||
#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)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Leiste (Handy): Fahrstuhl-Zeile mit Balken ---------- */
|
/* ---------- Leiste (Handy): Fahrstuhl-Zeile mit Linie ----------
|
||||||
|
Unter dem aktiven Wort läuft eine dünne schwarze Linie (der Gleiter), das Wort selbst wird dunkel und fett. Schwarze Flächen bleiben den Knöpfen
|
||||||
|
vorbehalten. Die weiße Overlay-Kopie der Wörter aus reiter.js wird dafür nicht mehr gezeigt. */
|
||||||
|
|
||||||
.kn-leiste { display: none; }
|
.kn-leiste { display: none; }
|
||||||
.kn-leiste-reel { height: var(--kn-zeile-h); overflow: hidden; }
|
.kn-leiste-reel { height: var(--kn-zeile-h); overflow: hidden; }
|
||||||
@@ -144,25 +150,18 @@
|
|||||||
.kn-balken { position: relative; flex: 1; min-width: 0; height: 44px; padding-right: 6px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; } /* 6 px Luft rechts: der Zähler am letzten Wort ragt über das Wort hinaus und würde sonst abgeschnitten */
|
.kn-balken { position: relative; flex: 1; min-width: 0; height: 44px; padding-right: 6px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; } /* 6 px Luft rechts: der Zähler am letzten Wort ragt über das Wort hinaus und würde sonst abgeschnitten */
|
||||||
.kn-balken::-webkit-scrollbar { display: none; }
|
.kn-balken::-webkit-scrollbar { display: none; }
|
||||||
.kn-balken-basis { position: relative; z-index: 1; display: flex; width: max-content; min-width: 100%; height: 44px; }
|
.kn-balken-basis { position: relative; z-index: 1; display: flex; width: max-content; min-width: 100%; height: 44px; }
|
||||||
.kn-gleiter { position: absolute; z-index: 2; top: 4px; left: 0; height: 36px; overflow: hidden; border-radius: 6px; background: var(--kn-accent); pointer-events: none; transition: transform 550ms var(--kn-feder), width 550ms var(--kn-feder); }
|
.kn-gleiter { position: absolute; z-index: 2; bottom: 2px; left: 0; height: 2px; overflow: hidden; border-radius: 1px; background: var(--kn-accent); pointer-events: none; transition: transform 550ms var(--kn-feder), width 550ms var(--kn-feder); }
|
||||||
.kn-overlay { position: absolute; top: -4px; left: 0; display: flex; height: 44px; transition: transform 550ms var(--kn-feder); }
|
.kn-overlay { display: none; }
|
||||||
.kn-fb .kn-overlay { display: none; }
|
|
||||||
.kn-fb .kn-gleiter { z-index: 0; }
|
|
||||||
.kn-fb .kn-t[aria-selected="true"] { color: #fff; }
|
|
||||||
|
|
||||||
.kn-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 4px; font-size: 15px; font-weight: 600; color: var(--kn-text-2); white-space: nowrap; text-align: center; transition: color 250ms; }
|
.kn-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 4px; font-size: 15px; font-weight: 500; color: var(--kn-text-2); white-space: nowrap; text-align: center; transition: color 250ms; }
|
||||||
|
.kn-t[aria-selected="true"] { color: var(--kn-text); font-weight: 600; }
|
||||||
/* Fokusring: .kn [tabindex]:focus-visible und .kn button:focus-visible (module.css, 0-3-0 und 0-2-1, später geladen) setzen +2px und schlugen die alte Regel
|
/* Fokusring: .kn [tabindex]:focus-visible und .kn button:focus-visible (module.css, 0-3-0 und 0-2-1, später geladen) setzen +2px und schlugen die alte Regel
|
||||||
(0-2-0); der Ring lag außerhalb und wurde vom Balken (overflow) abgeschnitten. Mit .kn und [role="tab"] (0-4-0) bleibt er innen. Der aktive Reiter liegt
|
(0-2-0); der Ring lag außerhalb und wurde vom Balken (overflow) abgeschnitten. Mit .kn und [role="tab"] (0-4-0) bleibt er innen. */
|
||||||
auf dem schwarzen Gleiter: dort zeichnet der Gleiter einen hellen Innenring. */
|
|
||||||
.kn .kn-t[role="tab"]:focus-visible { outline-offset: -2px; border-radius: 6px; }
|
.kn .kn-t[role="tab"]:focus-visible { outline-offset: -2px; border-radius: 6px; }
|
||||||
.kn .kn-balken:has(.kn-t:focus-visible) .kn-gleiter { outline: 2px solid #fff; outline-offset: -3px; }
|
|
||||||
.kn .kn-fb .kn-t[aria-selected="true"]:focus-visible { outline-color: #fff; }
|
|
||||||
.kn-schrift-klein .kn-t { font-size: 14px; }
|
.kn-schrift-klein .kn-t { font-size: 14px; }
|
||||||
.kn-overlay .kn-t { color: #fff; }
|
|
||||||
|
|
||||||
.kn-l { position: relative; line-height: 20px; }
|
.kn-l { position: relative; line-height: 20px; }
|
||||||
.kn-zahl { position: absolute; top: -9px; left: 100%; min-width: 16px; height: 16px; margin-left: -6px; padding: 0 4px; border-radius: 8px; background: #b3093c; color: #fff; font-size: 11px; line-height: 16px; font-weight: 700; text-align: center; box-shadow: 0 0 0 2px var(--kn-bg); animation: kn-zahl-ein 400ms cubic-bezier(.3,1.5,.5,1); }
|
.kn-zahl { position: absolute; top: -9px; left: 100%; min-width: 16px; height: 16px; margin-left: -6px; padding: 0 4px; border-radius: 8px; background: #b3093c; color: #fff; font-size: 11px; line-height: 16px; font-weight: 700; text-align: center; box-shadow: 0 0 0 2px var(--kn-bg); animation: kn-zahl-ein 400ms cubic-bezier(.3,1.5,.5,1); }
|
||||||
.kn-overlay .kn-zahl { box-shadow: 0 0 0 2px var(--kn-accent); }
|
|
||||||
.kn-reiter-t .kn-zahl { top: -7px; }
|
.kn-reiter-t .kn-zahl { top: -7px; }
|
||||||
.kn-zahl--weg { transform: scale(0); transition: transform 250ms; animation: none; }
|
.kn-zahl--weg { transform: scale(0); transition: transform 250ms; animation: none; }
|
||||||
@keyframes kn-zahl-ein { from { transform: scale(0); } }
|
@keyframes kn-zahl-ein { from { transform: scale(0); } }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const ETAGE_ANSICHTEN = {
|
|||||||
gutschrift: ['/neu/ansicht-gutschrift.js', 'zeigeGutschrift', 'etage-body'],
|
gutschrift: ['/neu/ansicht-gutschrift.js', 'zeigeGutschrift', 'etage-body'],
|
||||||
wechsel: ['/neu/ansicht-wechsel.js', 'zeigeWechsel', 'etage-body'],
|
wechsel: ['/neu/ansicht-wechsel.js', 'zeigeWechsel', 'etage-body'],
|
||||||
bestellen: ['/neu/ansicht-bestellen.js', 'zeigeBestellen', 'etage-body'],
|
bestellen: ['/neu/ansicht-bestellen.js', 'zeigeBestellen', 'etage-body'],
|
||||||
|
speicher: ['/neu/ansicht-speicher.js', 'zeigeSpeicher', 'etage-body'], // Navigation leichter V7: Unterseite statt Reiter E-Mail
|
||||||
}
|
}
|
||||||
// Gemeinsame Funktionsmodule, wörtlich aus app.js:1938-1945 (Wurzel: der Behälter mit der alten ID)
|
// Gemeinsame Funktionsmodule, wörtlich aus app.js:1938-1945 (Wurzel: der Behälter mit der alten ID)
|
||||||
export const FUNKTIONS_MODULE = {
|
export const FUNKTIONS_MODULE = {
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben.
|
/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben.
|
||||||
Reihenfolge der Zeilen (V3): Passwort ändern, Datenschutz, Impressum, App installieren (nur wenn sinnvoll), Verträge kündigen,
|
Reihenfolge der Zeilen (V3, geändert mit „Navigation leichter“ V8): Passwort ändern, App installieren (nur wenn sinnvoll), Verträge kündigen,
|
||||||
Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden. Namen und Mails kommen nur als Text in die Seite.
|
Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden; darunter klein Datenschutz und Impressum. Namen und Mails kommen nur als Text in die Seite.
|
||||||
Das Sheet ist ein Menü: Esc, ein Tipp aufs Dunkle und „Schließen“ schließen es; Zurück schließt es über das Overlay des Routers. */
|
Das Sheet ist ein Menü: Esc, ein Tipp aufs Dunkle und „Schließen“ schließen es; Zurück schließt es über das Overlay des Routers. */
|
||||||
|
|
||||||
import { h, leeren } from './dom.js'
|
import { h, leeren } from './dom.js'
|
||||||
@@ -22,17 +22,17 @@ export function installAngebot({ handy, standalone, prompt, ios }) {
|
|||||||
|
|
||||||
const HINWEIS_IOS = 'Tippe auf Teilen und dann auf Zum Home-Bildschirm.'
|
const HINWEIS_IOS = 'Tippe auf Teilen und dann auf Zum Home-Bildschirm.'
|
||||||
|
|
||||||
/** Zeilen des Kontos in der Reihenfolge von V3. */
|
/** Zeilen des Kontos: vier volle Zeilen, Datenschutz und Impressum klein darunter (klein: true). */
|
||||||
export function kontoEintraege({ fristen, install } = {}) {
|
export function kontoEintraege({ fristen, install } = {}) {
|
||||||
const frist = fristen?.anzeigen === true
|
const frist = fristen?.anzeigen === true
|
||||||
return [
|
return [
|
||||||
{ id: 'passwort', text: 'Passwort ändern', href: '#passwort' },
|
{ id: 'passwort', text: 'Passwort ändern', href: '#passwort' },
|
||||||
{ id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true },
|
|
||||||
{ id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true },
|
|
||||||
...(install ? [{ id: 'installieren', text: 'App installieren', ...(install === 'ios' ? { hinweis: HINWEIS_IOS } : {}) }] : []),
|
...(install ? [{ id: 'installieren', text: 'App installieren', ...(install === 'ios' ? { hinweis: HINWEIS_IOS } : {}) }] : []),
|
||||||
{ id: 'kuendigen', text: 'Verträge kündigen', href: '/kuendigen.html', betont: true },
|
{ id: 'kuendigen', text: 'Verträge kündigen', href: '/kuendigen.html', betont: true },
|
||||||
{ id: 'widerrufen', text: 'Vertrag widerrufen', href: '/widerrufen.html', betont: true, ...(frist ? { pille: 'Frist läuft' } : {}) },
|
{ id: 'widerrufen', text: 'Vertrag widerrufen', href: '/widerrufen.html', betont: true, ...(frist ? { pille: 'Frist läuft' } : {}) },
|
||||||
{ id: 'abmelden', text: 'Abmelden' },
|
{ id: 'abmelden', text: 'Abmelden' },
|
||||||
|
{ id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true, klein: true },
|
||||||
|
{ id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true, klein: true },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function baueKonto({ me, fristen, install, onAbmelden, onInstall, overlay
|
|||||||
|
|
||||||
function zeile(e) {
|
function zeile(e) {
|
||||||
const inhalt = [h('span', { class: 'kn-konto-text' }, e.text, e.hinweis ? h('span', { class: 'kn-konto-hinweis' }, e.hinweis) : null), e.pille ? h('span', { class: 'kn-pille kn-pille-orange' }, e.pille) : null]
|
const inhalt = [h('span', { class: 'kn-konto-text' }, e.text, e.hinweis ? h('span', { class: 'kn-konto-hinweis' }, e.hinweis) : null), e.pille ? h('span', { class: 'kn-pille kn-pille-orange' }, e.pille) : null]
|
||||||
const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}`
|
const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}${e.klein ? ' kn-konto-zeile--klein' : ''}`
|
||||||
if (e.href === undefined) return h('button', { class: klasse, type: 'button', 'data-konto': e.id, on: { click: () => (e.id === 'abmelden' ? onAbmelden() : onInstall?.()) } }, inhalt)
|
if (e.href === undefined) return h('button', { class: klasse, type: 'button', 'data-konto': e.id, on: { click: () => (e.id === 'abmelden' ? onAbmelden() : onInstall?.()) } }, inhalt)
|
||||||
const intern = e.href.startsWith('#')
|
const intern = e.href.startsWith('#')
|
||||||
return h('a', { class: klasse, href: e.href, 'data-konto': e.id, target: e.neuerTab ? '_blank' : null, rel: e.neuerTab ? 'noopener' : null,
|
return h('a', { class: klasse, href: e.href, 'data-konto': e.id, target: e.neuerTab ? '_blank' : null, rel: e.neuerTab ? 'noopener' : null,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und schwarzem Balken (reiter.js, art 'balken').
|
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und einer dünnen Linie unter dem aktiven Wort (reiter.js, art 'balken').
|
||||||
Desktop: Menü links, Wörter ohne Icons. Beide zeigen dieselben Zähler. Dazu der reine Zustand der Bildschirmtastatur
|
Desktop: Menü links, Wörter ohne Icons. Beide zeigen dieselben Zähler. Dazu der reine Zustand der Bildschirmtastatur
|
||||||
(Leiste und Streifen verschwinden nur bei Texteingabe) und seine Verdrahtung mit visualViewport, focusin und focusout. */
|
(Leiste und Streifen verschwinden nur bei Texteingabe) und seine Verdrahtung mit visualViewport, focusin und focusout. */
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -109,14 +109,15 @@
|
|||||||
.kn .tm-reiter:focus-visible { outline-offset: -2px; }
|
.kn .tm-reiter:focus-visible { outline-offset: -2px; }
|
||||||
.kn .tm-panel:focus { outline: none; }
|
.kn .tm-panel:focus { outline: none; }
|
||||||
|
|
||||||
/* ---- Werkzeugleiste: Ansicht, Pfeile, Sprung, Filter ---- */
|
/* ---- Werkzeugleiste: Ansicht, Pfeile, Sprung, Filter ----
|
||||||
|
Navigation leichter (V5): Schwarz nur für die Hauptaktion. Die Ansicht (Tag/Woche/Monat) ist ein hellgrauer Umschalter mit weißer Auswahl,
|
||||||
|
die Filter sind leise Chips mit grauer Auswahl; schwarz bleibt „+ Termin eintragen“. */
|
||||||
.kn .tm-leiste { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; }
|
.kn .tm-leiste { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; }
|
||||||
.kn .tm-leiste [hidden], .kn .tm-filter[hidden] { display: none; }
|
.kn .tm-leiste [hidden], .kn .tm-filter[hidden] { display: none; }
|
||||||
.kn .tm-segment { display: inline-flex; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); overflow: hidden; }
|
.kn .tm-segment { display: inline-flex; padding: 2px; border-radius: 8px; background: var(--kn-bg-subtle); box-shadow: inset 0 0 0 1px var(--kn-line); overflow: hidden; }
|
||||||
.kn .tm-seg { min-height: 32px; padding: 0 14px; border: 0; background: none; color: var(--kn-text); font: inherit; font-weight: 500; cursor: pointer; transition: background-color .12s; }
|
.kn .tm-seg { min-height: 28px; padding: 0 14px; border: 0; border-radius: 6px; background: none; color: var(--kn-text-2); font: inherit; font-weight: 500; cursor: pointer; transition: background-color .12s, color .12s; }
|
||||||
.kn .tm-seg + .tm-seg { box-shadow: -1px 0 0 var(--kn-line); }
|
.kn .tm-seg:hover { color: var(--kn-text); }
|
||||||
.kn .tm-seg:hover { background: var(--kn-bg-hover); }
|
.kn .tm-seg[aria-pressed='true'] { background: var(--kn-bg); color: var(--kn-text); font-weight: 600; box-shadow: var(--kn-ring); }
|
||||||
.kn .tm-seg[aria-pressed='true'] { background: var(--kn-accent); color: #fff; }
|
|
||||||
.kn .tm-nav { display: inline-flex; align-items: center; gap: 6px; }
|
.kn .tm-nav { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.kn .tm-heute { min-width: 72px; }
|
.kn .tm-heute { min-width: 72px; }
|
||||||
.kn .tm-pfeil { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); font: inherit; font-size: 18px; line-height: 1; cursor: pointer; }
|
.kn .tm-pfeil { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); font: inherit; font-size: 18px; line-height: 1; cursor: pointer; }
|
||||||
@@ -124,9 +125,9 @@
|
|||||||
.kn .tm-sprung { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--kn-text-2); }
|
.kn .tm-sprung { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--kn-text-2); }
|
||||||
.kn .tm-zeitraum { margin: 20px 0 8px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
.kn .tm-zeitraum { margin: 20px 0 8px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||||
.kn .tm-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
.kn .tm-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
|
||||||
.kn .tm-chip { min-height: 28px; padding: 0 12px; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color .12s; }
|
.kn .tm-chip { min-height: 28px; padding: 0 10px; border: 0; border-radius: var(--kn-radius); background: none; color: var(--kn-text-2); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color .12s, color .12s; }
|
||||||
.kn .tm-chip:hover { background: var(--kn-bg-hover); }
|
.kn .tm-chip:hover { background: var(--kn-bg-hover); color: var(--kn-text); }
|
||||||
.kn .tm-chip[aria-pressed='true'] { background: var(--kn-accent); color: #fff; }
|
.kn .tm-chip[aria-pressed='true'] { background: var(--kn-grau-bg); color: var(--kn-text); font-weight: 600; }
|
||||||
.kn .tm-werkzeug-zeile { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
|
.kn .tm-werkzeug-zeile { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
|
||||||
.kn .tm-werkzeuge { display: contents; }
|
.kn .tm-werkzeuge { display: contents; }
|
||||||
.kn .tm-werkzeuge button[aria-expanded='true'] { background: var(--kn-bg-hover); }
|
.kn .tm-werkzeuge button[aria-expanded='true'] { background: var(--kn-bg-hover); }
|
||||||
@@ -340,6 +341,8 @@
|
|||||||
.kn .tm-leiste { gap: 8px; }
|
.kn .tm-leiste { gap: 8px; }
|
||||||
.kn .tm-segment { width: 100%; }
|
.kn .tm-segment { width: 100%; }
|
||||||
.kn .tm-seg { flex: 1; padding-left: 8px; padding-right: 8px; }
|
.kn .tm-seg { flex: 1; padding-left: 8px; padding-right: 8px; }
|
||||||
|
.kn .tm-filter { gap: 2px; } /* die fünf leisen Chips passen auf 390 px in eine Zeile */
|
||||||
|
.kn .tm-chip { padding-left: 8px; padding-right: 8px; }
|
||||||
.kn .tm-nav { width: 100%; }
|
.kn .tm-nav { width: 100%; }
|
||||||
.kn .tm-nav .tm-heute { flex: 1; }
|
.kn .tm-nav .tm-heute { flex: 1; }
|
||||||
.kn .tm-sprung { width: 100%; }
|
.kn .tm-sprung { width: 100%; }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* Kundenansicht neu: gemeinsame Reiter. Zwei Arten mit demselben Baustein:
|
/* Kundenansicht neu: gemeinsame Reiter. Zwei Arten mit demselben Baustein:
|
||||||
'unterstrich' = Reiter einer Hauptseite (basis.css, .kn-reiter), 'balken' = Fahrstuhl-Zeile und Reiter in Etage 2:
|
'unterstrich' = Reiter einer Hauptseite (basis.css, .kn-reiter), 'balken' = Fahrstuhl-Zeile und Reiter in Etage 2:
|
||||||
ein schwarzer Gleiter fährt federnd unter das aktive Wort, darin eine Overlay-Kopie der Wörter in Weiß mit Gegenbewegung
|
ein Gleiter fährt federnd unter das aktive Wort, darin eine Overlay-Kopie der Wörter in Weiß mit Gegenbewegung
|
||||||
(final-2-frei.html). Das Overlay ist aria-hidden und trägt keine Rollen. Zahlenfunktionen sind rein und werden mit node --test geprüft;
|
(final-2-frei.html). Seit „Navigation leichter“ (V3) ist der Gleiter in huelle.css eine dünne Linie und das Overlay ausgeblendet;
|
||||||
|
Messen und Rückfall laufen unverändert weiter. Das Overlay ist aria-hidden und trägt keine Rollen. Zahlenfunktionen sind rein und werden mit node --test geprüft;
|
||||||
Messen und Bewegen braucht getBoundingClientRect und entfällt ohne Browser. */
|
Messen und Bewegen braucht getBoundingClientRect und entfällt ohne Browser. */
|
||||||
|
|
||||||
import { h } from './dom.js'
|
import { h } from './dom.js'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const ROUTEN = Object.freeze(Object.fromEntries(Object.entries({
|
|||||||
wartung: { bereich: 'vertraege', reiter: 'abo' },
|
wartung: { bereich: 'vertraege', reiter: 'abo' },
|
||||||
rechnungen: { bereich: 'vertraege', reiter: 'rechnungen' },
|
rechnungen: { bereich: 'vertraege', reiter: 'rechnungen' },
|
||||||
stunden: { bereich: 'vertraege', reiter: 'stunden' },
|
stunden: { bereich: 'vertraege', reiter: 'stunden' },
|
||||||
speicher: { bereich: 'vertraege', reiter: 'email' },
|
speicher: { bereich: 'vertraege', etage: 'speicher' },
|
||||||
gutschrift: { bereich: 'vertraege', etage: 'gutschrift' },
|
gutschrift: { bereich: 'vertraege', etage: 'gutschrift' },
|
||||||
wechsel: { bereich: 'vertraege', etage: 'wechsel' },
|
wechsel: { bereich: 'vertraege', etage: 'wechsel' },
|
||||||
'bestellen-hosting': { bereich: 'vertraege', etage: 'bestellen', produkt: 'hosting' },
|
'bestellen-hosting': { bereich: 'vertraege', etage: 'bestellen', produkt: 'hosting' },
|
||||||
@@ -42,6 +42,7 @@ const TITEL_BEREICH = { website: 'Website', funktionen: 'Funktionen', chat: 'Cha
|
|||||||
const TITEL_ETAGE = {
|
const TITEL_ETAGE = {
|
||||||
projekt: 'Projekt', news: 'News', termine: 'Terminplaner', webshop: 'Webshop', buchungen: 'Buchungskalender', zeiten: 'Öffnungszeiten',
|
projekt: 'Projekt', news: 'News', termine: 'Terminplaner', webshop: 'Webshop', buchungen: 'Buchungskalender', zeiten: 'Öffnungszeiten',
|
||||||
analyse: 'Analyse', gutschrift: 'Übernahmegutschrift', wechsel: 'Abo wechseln', bestellen: 'Abo bestellen', passwort: 'Passwort ändern',
|
analyse: 'Analyse', gutschrift: 'Übernahmegutschrift', wechsel: 'Abo wechseln', bestellen: 'Abo bestellen', passwort: 'Passwort ändern',
|
||||||
|
speicher: 'E-Mail-Speicher',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alter data-view-Name (VIEW_HASHES, app.js:1933) → Hash-Name
|
// Alter data-view-Name (VIEW_HASHES, app.js:1933) → Hash-Name
|
||||||
@@ -107,8 +108,8 @@ export function titelFuer(route) {
|
|||||||
return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? ''
|
return TITEL_ETAGE[route.etage] ?? TITEL_BEREICH[route.bereich] ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Etagen, auf denen etwas bestellt wird: Abo bestellen, Wechsel und die Übernahmegutschrift. Dazu der Reiter E-Mail der Verträge (Zusatzspeicher). */
|
/** Etagen, auf denen etwas bestellt wird: Abo bestellen, Wechsel, die Übernahmegutschrift und der E-Mail-Speicher (Zusatzspeicher; bis „Navigation leichter“ V7 ein Reiter der Verträge). */
|
||||||
const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift']
|
const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift', 'speicher']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steht auf dieser Seite der Widerrufsstreifen? Nicht auf den Bestellseiten: Dort müssen Kerninfo und Knopf „Zahlungspflichtig bestellen“ auf einem
|
* Steht auf dieser Seite der Widerrufsstreifen? Nicht auf den Bestellseiten: Dort müssen Kerninfo und Knopf „Zahlungspflichtig bestellen“ auf einem
|
||||||
@@ -117,6 +118,5 @@ const BESTELL_ETAGEN = ['bestellen', 'wechsel', 'gutschrift']
|
|||||||
*/
|
*/
|
||||||
export function zeigtWiderrufsstreifen(route) {
|
export function zeigtWiderrufsstreifen(route) {
|
||||||
if (!route) return true
|
if (!route) return true
|
||||||
if (BESTELL_ETAGEN.includes(route.etage)) return false
|
return !BESTELL_ETAGEN.includes(route.etage)
|
||||||
return !(route.bereich === 'vertraege' && route.reiter === 'email')
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { api } from './api.js'
|
import { api } from './api.js'
|
||||||
import { h, leeren } from './dom.js'
|
import { h, leeren } from './dom.js'
|
||||||
import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr } from './ansicht-hilfen.js'
|
import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse, beiRueckkehr, navigiere } from './ansicht-hilfen.js'
|
||||||
|
|
||||||
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
|
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
|
||||||
const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.'
|
const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.'
|
||||||
@@ -113,7 +113,7 @@ function vertragsBlock(daten, sub, viewAs) {
|
|||||||
fakt(gekuendigt(sub) ? 'Leistungen' : 'Abrechnung', aboZeile(sub)),
|
fakt(gekuendigt(sub) ? 'Leistungen' : 'Abrechnung', aboZeile(sub)),
|
||||||
fakt('Laufzeit', laufzeitText(sub)),
|
fakt('Laufzeit', laufzeitText(sub)),
|
||||||
sub.offerText ? fakt('Angebot', sub.offerText) : null,
|
sub.offerText ? fakt('Angebot', sub.offerText) : null,
|
||||||
fakt('E-Mail-Speicher', `${gb} GB${einheiten ? ` (${einheiten} Zusatzpaket${einheiten === 1 ? '' : 'e'})` : ''}`)),
|
fakt('E-Mail-Speicher', [`${gb} GB${einheiten ? ` (${einheiten} Zusatzpaket${einheiten === 1 ? '' : 'e'})` : ''} · `, speicherLink()])),
|
||||||
leistungen(katalog?.features),
|
leistungen(katalog?.features),
|
||||||
h('p', { class: 'kn-links' },
|
h('p', { class: 'kn-links' },
|
||||||
h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Vertragsbedingungen'), ' · ',
|
h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Vertragsbedingungen'), ' · ',
|
||||||
@@ -122,6 +122,12 @@ function vertragsBlock(daten, sub, viewAs) {
|
|||||||
sub.paymentProblem ? zahlungsKnopf(viewAs) : null)
|
sub.paymentProblem ? zahlungsKnopf(viewAs) : null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** „ändern“ am E-Mail-Speicher: öffnet die Unterseite #speicher (Navigation leichter V7). Mit Zusatztaste bleibt es ein normaler Link. */
|
||||||
|
function speicherLink() {
|
||||||
|
const ohneZusatz = (ev) => !(ev.defaultPrevented || ev.button > 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
||||||
|
return h('a', { href: '#speicher', 'data-aktion': 'speicher', on: { click: (ev) => { if (!ohneZusatz(ev)) return; ev.preventDefault(); navigiere('#speicher') } } }, 'ändern')
|
||||||
|
}
|
||||||
|
|
||||||
function gutschriftLink() {
|
function gutschriftLink() {
|
||||||
const badge = h('span', { class: 'kn-badge', 'data-badge': true, hidden: true })
|
const badge = h('span', { class: 'kn-badge', 'data-badge': true, hidden: true })
|
||||||
const link = h('a', { class: `${K.knopf} kn-gutschrift-link`, ...zuAnsicht('takeover') }, 'Übernahmegutschrift beantragen')
|
const link = h('a', { class: `${K.knopf} kn-gutschrift-link`, ...zuAnsicht('takeover') }, 'Übernahmegutschrift beantragen')
|
||||||
|
|||||||
@@ -3,24 +3,23 @@ import assert from 'node:assert/strict'
|
|||||||
import { zeigeVertraege, sichtbareReiter, waehleReiter, AUF_ABO_WARTEN } from '../public/neu/ansicht-vertraege.js'
|
import { zeigeVertraege, sichtbareReiter, waehleReiter, AUF_ABO_WARTEN } from '../public/neu/ansicht-vertraege.js'
|
||||||
import { merkeHinweis } from '../public/neu/ansicht-hilfen.js'
|
import { merkeHinweis } from '../public/neu/ansicht-hilfen.js'
|
||||||
import { routeAusHash } from '../public/neu/routen.js'
|
import { routeAusHash } from '../public/neu/routen.js'
|
||||||
import { mit, me, abo, aboAntwort, rechnung, neueWurzel, LIMITS } from './support/knDaten.js'
|
import { mit, me, abo, aboAntwort, ledger, rechnung, neueWurzel, LIMITS } from './support/knDaten.js'
|
||||||
|
|
||||||
const VIER = ['rechnungen', 'abo', 'stunden', 'email']
|
const ZWEI = ['abo', 'rechnungen']
|
||||||
test('sichtbareReiter nach Abo', () => {
|
test('sichtbareReiter: immer Abo und Rechnungen (Navigation leichter V7), unabhängig vom Abo', () => {
|
||||||
assert.deepEqual([null, aboAntwort(), aboAntwort({ subscription: abo({ active: false }) })].map((d) => sichtbareReiter(d)), [['rechnungen', 'abo'], ['rechnungen', 'abo'], ['rechnungen', 'abo']])
|
const faelle = [null, aboAntwort(), aboAntwort({ subscription: abo({ active: false }) }), aboAntwort({ subscription: abo({ produkt: 'hosting' }) }), aboAntwort({ subscription: abo() })]
|
||||||
assert.deepEqual(sichtbareReiter(aboAntwort({ subscription: abo({ produkt: 'hosting' }) })), ['rechnungen', 'abo', 'email'])
|
for (const d of faelle) assert.deepEqual(sichtbareReiter(d), ZWEI)
|
||||||
assert.deepEqual(sichtbareReiter(aboAntwort({ subscription: abo() })), VIER)
|
|
||||||
})
|
})
|
||||||
test('Vorbedingung (VF8): der Bereichs-Hash hat keinen Reiter, die Reiter-Hashes haben einen', () => {
|
test('Vorbedingung (VF8): der Bereichs-Hash hat keinen Reiter, die Reiter-Hashes haben einen', () => {
|
||||||
assert.equal(routeAusHash('#vertraege').reiter, undefined)
|
assert.equal(routeAusHash('#vertraege').reiter, undefined)
|
||||||
for (const [hash, reiter] of [['#abo', 'abo'], ['#wartung', 'abo'], ['#rechnungen', 'rechnungen'], ['#stunden', 'stunden'], ['#speicher', 'email']]) assert.equal(routeAusHash(hash).reiter, reiter, hash)
|
for (const [hash, reiter] of [['#abo', 'abo'], ['#wartung', 'abo'], ['#rechnungen', 'rechnungen'], ['#stunden', 'stunden']]) assert.equal(routeAusHash(hash).reiter, reiter, hash)
|
||||||
|
assert.deepEqual(routeAusHash('#speicher'), { bereich: 'vertraege', etage: 'speicher', query: '' }) // der Speicher ist eine Unterseite
|
||||||
})
|
})
|
||||||
test('waehleReiter: nur der Bereichs-Einstieg springt, jede ausdrückliche Wahl bleibt (Review Focus 2)', () => {
|
test('waehleReiter: nur der Bereichs-Einstieg springt, jede ausdrückliche Wahl bleibt (Review Focus 2)', () => {
|
||||||
const w = (o) => waehleReiter({ routeReiter: undefined, sichtbar: VIER, offeneRechnungen: 1, ...o })
|
const w = (o) => waehleReiter({ routeReiter: undefined, sichtbar: ZWEI, offeneRechnungen: 1, ...o })
|
||||||
assert.equal(w({}), 'rechnungen'); assert.equal(w({ offeneRechnungen: 0 }), 'abo')
|
assert.equal(w({}), 'rechnungen'); assert.equal(w({ offeneRechnungen: 0 }), 'abo')
|
||||||
assert.equal(w({ routeReiter: 'abo' }), 'abo') // #abo, #wartung und der Klick auf „Abo“ trotz offener Rechnung
|
assert.equal(w({ routeReiter: 'abo' }), 'abo') // #abo, #wartung und der Klick auf „Abo“ trotz offener Rechnung
|
||||||
assert.equal(w({ routeReiter: 'stunden' }), 'stunden'); assert.equal(w({ routeReiter: 'email' }), 'email'); assert.equal(w({ routeReiter: 'rechnungen', offeneRechnungen: 0 }), 'rechnungen')
|
assert.equal(w({ routeReiter: 'stunden' }), 'abo'); assert.equal(w({ routeReiter: 'rechnungen', offeneRechnungen: 0 }), 'rechnungen') // #stunden öffnet das Abo
|
||||||
assert.equal(w({ routeReiter: 'stunden', sichtbar: ['rechnungen', 'abo'] }), 'abo')
|
|
||||||
})
|
})
|
||||||
const SERVER = { 'GET /api/abo': aboAntwort(), 'GET /api/invoices': { invoices: [rechnung({ balance: 50 })], incomplete: false }, 'GET /api/auftraege': { aktiv: false }, 'GET /api/abo/takeover-credits': { requests: [], limits: LIMITS } }
|
const SERVER = { 'GET /api/abo': aboAntwort(), 'GET /api/invoices': { invoices: [rechnung({ balance: 50 })], incomplete: false }, 'GET /api/auftraege': { aktiv: false }, 'GET /api/abo/takeover-credits': { requests: [], limits: LIMITS } }
|
||||||
const zeige = async (fx, route = {}) => { const w = neueWurzel(fx); await zeigeVertraege(w, { route: { bereich: 'vertraege', query: '', ...route }, me: me(), nav: 'tab' }); await fx.ruhe(); return w }
|
const zeige = async (fx, route = {}) => { const w = neueWurzel(fx); await zeigeVertraege(w, { route: { bereich: 'vertraege', query: '', ...route }, me: me(), nav: 'tab' }); await fx.ruhe(); return w }
|
||||||
@@ -29,7 +28,7 @@ const WOHIN = (fx) => { fx.window.history.replaceState = (_s, _t, url) => { fx.w
|
|||||||
test('Verträge: Reiter in fester Reihenfolge, Kündigen-Link unter jedem Reiter, ausdrückliche Reiterwahl bleibt trotz offener Rechnung', mit(SERVER, async (fx) => {
|
test('Verträge: Reiter in fester Reihenfolge, Kündigen-Link unter jedem Reiter, ausdrückliche Reiterwahl bleibt trotz offener Rechnung', mit(SERVER, async (fx) => {
|
||||||
for (const [route, gewaehlt] of [[{ reiter: 'abo' }, 'Abo'], [{ reiter: 'rechnungen' }, 'Rechnungen'], [{}, 'Rechnungen']]) { // {} = Bereichs-Einstieg
|
for (const [route, gewaehlt] of [[{ reiter: 'abo' }, 'Abo'], [{ reiter: 'rechnungen' }, 'Rechnungen'], [{}, 'Rechnungen']]) { // {} = Bereichs-Einstieg
|
||||||
const w = await zeige(fx, route)
|
const w = await zeige(fx, route)
|
||||||
assert.deepEqual(fx.alle(w, '[role="tab"]').map((t) => t.textContent), ['Rechnungen', 'Abo'])
|
assert.deepEqual(fx.alle(w, '[role="tab"]').map((t) => t.textContent), ['Abo', 'Rechnungen'])
|
||||||
assert.equal(aktiv(fx, w), gewaehlt)
|
assert.equal(aktiv(fx, w), gewaehlt)
|
||||||
assert.equal(fx.finde(w, 'a[href="/kuendigen.html"]').textContent, 'Verträge hier kündigen')
|
assert.equal(fx.finde(w, 'a[href="/kuendigen.html"]').textContent, 'Verträge hier kündigen')
|
||||||
}
|
}
|
||||||
@@ -38,9 +37,10 @@ test('Verträge: die Route entscheidet, die Adressleiste nicht (keine zweite Wah
|
|||||||
fx.window.location.hash = '#vertraege'; assert.equal(aktiv(fx, await zeige(fx, { reiter: 'abo' })), 'Abo') // widerspricht der Route absichtlich
|
fx.window.location.hash = '#vertraege'; assert.equal(aktiv(fx, await zeige(fx, { reiter: 'abo' })), 'Abo') // widerspricht der Route absichtlich
|
||||||
fx.window.location.hash = '#abo'; assert.equal(aktiv(fx, await zeige(fx, {})), 'Rechnungen')
|
fx.window.location.hash = '#abo'; assert.equal(aktiv(fx, await zeige(fx, {})), 'Rechnungen')
|
||||||
}))
|
}))
|
||||||
test('Reiterklick navigiert ausdrücklich und ersetzt: #rechnungen, #abo, #stunden, #speicher', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo() }) }, async (fx) => {
|
test('Reiterklick navigiert ausdrücklich und ersetzt: #abo, #rechnungen; auch mit Wartungs-Abo nur zwei Reiter', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo() }) }, async (fx) => {
|
||||||
WOHIN(fx); const w = await zeige(fx, { reiter: 'rechnungen' })
|
WOHIN(fx); const w = await zeige(fx, { reiter: 'rechnungen' })
|
||||||
for (const [label, hash] of [['Abo', '#abo'], ['Stunden', '#stunden'], ['E-Mail', '#speicher'], ['Rechnungen', '#rechnungen']]) {
|
assert.deepEqual(fx.alle(w, '[role="tab"]').map((t) => t.textContent), ['Abo', 'Rechnungen'])
|
||||||
|
for (const [label, hash] of [['Abo', '#abo'], ['Rechnungen', '#rechnungen']]) {
|
||||||
await fx.klick(fx.finde(w, (e) => e.getAttribute?.('role') === 'tab' && e.textContent === label)); assert.equal(fx.window.location.hash, hash, label)
|
await fx.klick(fx.finde(w, (e) => e.getAttribute?.('role') === 'tab' && e.textContent === label)); assert.equal(fx.window.location.hash, hash, label)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -48,9 +48,33 @@ test('Verträge: /api/abo fällt aus, Reiter, Rechnungen und Kündigen bleiben',
|
|||||||
const w = await zeige(fx, { reiter: 'rechnungen' })
|
const w = await zeige(fx, { reiter: 'rechnungen' })
|
||||||
assert.match(fx.text(w), /Deine Verträge konnten gerade nicht geladen werden\./); assert.ok(fx.finde(w, 'a[href="/kuendigen.html"]')); assert.ok(fx.finde(w, 'tbody tr'))
|
assert.match(fx.text(w), /Deine Verträge konnten gerade nicht geladen werden\./); assert.ok(fx.finde(w, 'a[href="/kuendigen.html"]')); assert.ok(fx.finde(w, 'tbody tr'))
|
||||||
}))
|
}))
|
||||||
test('route.reiter stunden ohne Wartung: Abo-Reiter mit Hinweis', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo({ produkt: 'hosting' }) }) }, async (fx) => {
|
test('route.reiter stunden ohne Wartung: Abo-Reiter mit Hinweis, kein Stundenkonto', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo({ produkt: 'hosting' }) }) }, async (fx) => {
|
||||||
const w = await zeige(fx, { reiter: 'stunden' })
|
const w = await zeige(fx, { reiter: 'stunden' })
|
||||||
assert.equal(aktiv(fx, w), 'Abo'); assert.match(fx.text(w), /Ein Stundenkonto gibt es im Wartungs-Abo\./)
|
assert.equal(aktiv(fx, w), 'Abo'); assert.match(fx.text(w), /Ein Stundenkonto gibt es im Wartungs-Abo\./)
|
||||||
|
assert.equal(fx.finde(w, '#kn-stundenkonto'), null)
|
||||||
|
}))
|
||||||
|
const LEDGER = ledger({ balanceMinutes: 90, usedMinutes: 120, usage: [], credits: [] })
|
||||||
|
test('Navigation leichter V7: Wartungs-Abo zeigt das Stundenkonto unter dem Abo, Warnungen und Bedingungen-Link nur einmal', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo({ paymentProblem: true }), ledger: LEDGER }) }, async (fx) => {
|
||||||
|
for (const route of [{ reiter: 'abo' }, { reiter: 'stunden' }]) {
|
||||||
|
const w = await zeige(fx, route)
|
||||||
|
assert.equal(aktiv(fx, w), 'Abo')
|
||||||
|
const konto = fx.finde(w, '#kn-stundenkonto')
|
||||||
|
assert.ok(konto, 'Abschnitt Stundenkonto'); assert.equal(fx.finde(konto, 'h2').textContent, 'Stundenkonto')
|
||||||
|
assert.match(fx.text(konto), /verfügbare Zeit/); assert.match(fx.text(konto), /Verlauf/)
|
||||||
|
assert.equal(fx.text(w).split('Die letzte Abbuchung ist fehlgeschlagen').length - 1, 1, 'Zahlungswarnung genau einmal')
|
||||||
|
assert.doesNotMatch(fx.text(konto), /Leistungen & Bedingungen/)
|
||||||
|
assert.doesNotMatch(fx.text(w), /Ein Stundenkonto gibt es im Wartungs-Abo/)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
let speicherAbo = null
|
||||||
|
test('Navigation leichter V7: „ändern“ am E-Mail-Speicher führt auf #speicher, mit Hosting- und Wartungs-Abo', mit({ ...SERVER, 'GET /api/abo': () => ({ body: speicherAbo }) }, async (fx) => {
|
||||||
|
for (const produkt of ['hosting', 'wartung']) {
|
||||||
|
speicherAbo = aboAntwort({ subscription: abo({ produkt }), ledger: LEDGER })
|
||||||
|
WOHIN(fx); const w = await zeige(fx, { reiter: 'abo' })
|
||||||
|
const link = fx.finde(w, 'a[data-aktion="speicher"]')
|
||||||
|
assert.deepEqual([link.textContent, link.getAttribute('href')], ['ändern', '#speicher'], produkt)
|
||||||
|
await fx.klick(link); assert.equal(fx.window.location.hash, '#speicher', produkt)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
test('Hinweis aus einer Aktion steht genau einmal über den Reitern', mit(SERVER, async (fx) => {
|
test('Hinweis aus einer Aktion steht genau einmal über den Reitern', mit(SERVER, async (fx) => {
|
||||||
merkeHinweis('Dein Wechsel ist abgeschlossen.'); assert.match(fx.text(await zeige(fx, { reiter: 'abo' })), /Dein Wechsel ist abgeschlossen\./)
|
merkeHinweis('Dein Wechsel ist abgeschlossen.'); assert.match(fx.text(await zeige(fx, { reiter: 'abo' })), /Dein Wechsel ist abgeschlossen\./)
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ test('F3: Fokusring der Shell-Reiter liegt innen (Spezifität über .kn [tabinde
|
|||||||
// keine Regel mit niedrigerer Spezifität, die den Versatz setzt
|
// keine Regel mit niedrigerer Spezifität, die den Versatz setzt
|
||||||
assert.doesNotMatch(HUELLE, /\n\.kn-t:focus-visible/)
|
assert.doesNotMatch(HUELLE, /\n\.kn-t:focus-visible/)
|
||||||
assert.doesNotMatch(BASIS, /\n\.kn-reiter-t:focus-visible/)
|
assert.doesNotMatch(BASIS, /\n\.kn-reiter-t:focus-visible/)
|
||||||
// der aktive Reiter liegt auf dem schwarzen Gleiter: heller Innenring dort
|
// Navigation leichter V3: der Gleiter ist nur noch eine Linie unter dem Wort, der Ring des Reiters selbst genügt (kein heller Ring am Gleiter)
|
||||||
assert.match(HUELLE, /\.kn \.kn-balken:has\(\.kn-t:focus-visible\) \.kn-gleiter\s*\{[^}]*outline:\s*2px solid #fff[^}]*outline-offset:\s*-3px/)
|
assert.doesNotMatch(HUELLE, /\.kn-gleiter\s*\{[^}]*outline:\s*2px solid #fff/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('F3: Pflicht- und Zahlungslinks haben am Handy eine Tippfläche von 44 px', () => {
|
test('F3: Pflicht- und Zahlungslinks haben am Handy eine Tippfläche von 44 px', () => {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { h } from '../public/neu/dom.js'
|
|||||||
import { initialenVon, installAngebot, kontoEintraege, baueAvatar, baueKonto } from '../public/neu/konto.js'
|
import { initialenVon, installAngebot, kontoEintraege, baueAvatar, baueKonto } from '../public/neu/konto.js'
|
||||||
import { lies, handyTeil } from './support/knHuelle.js'
|
import { lies, handyTeil } from './support/knHuelle.js'
|
||||||
|
|
||||||
test('Reihenfolge, Texte und Ziele des Kontos (V3); Widerrufen immer da, "Frist läuft" nur in der Frist', () => {
|
test('Reihenfolge, Texte und Ziele des Kontos (V3 mit Navigation leichter V8); Widerrufen immer da, "Frist läuft" nur in der Frist', () => {
|
||||||
const ids = (o) => kontoEintraege(o).map((e) => e.id)
|
const ids = (o) => kontoEintraege(o).map((e) => e.id)
|
||||||
assert.deepEqual(ids({ fristen: null, install: null }), ['passwort', 'datenschutz', 'impressum', 'kuendigen', 'widerrufen', 'abmelden'])
|
assert.deepEqual(ids({ fristen: null, install: null }), ['passwort', 'kuendigen', 'widerrufen', 'abmelden', 'datenschutz', 'impressum'])
|
||||||
assert.deepEqual(ids({ fristen: null, install: 'ios' }), ['passwort', 'datenschutz', 'impressum', 'installieren', 'kuendigen', 'widerrufen', 'abmelden'])
|
assert.deepEqual(ids({ fristen: null, install: 'ios' }), ['passwort', 'installieren', 'kuendigen', 'widerrufen', 'abmelden', 'datenschutz', 'impressum'])
|
||||||
|
assert.deepEqual(kontoEintraege({ fristen: null, install: null }).filter((e) => e.klein).map((e) => e.id), ['datenschutz', 'impressum'])
|
||||||
const e = Object.fromEntries(kontoEintraege({ fristen: null, install: 'prompt' }).map((x) => [x.id, x]))
|
const e = Object.fromEntries(kontoEintraege({ fristen: null, install: 'prompt' }).map((x) => [x.id, x]))
|
||||||
assert.deepEqual([e.passwort.text, e.passwort.href, e.datenschutz.href, e.impressum.href, e.kuendigen.href, e.kuendigen.betont], ['Passwort ändern', '#passwort', '/datenschutz.html', 'https://webklar.com/impressum', '/kuendigen.html', true])
|
assert.deepEqual([e.passwort.text, e.passwort.href, e.datenschutz.href, e.impressum.href, e.kuendigen.href, e.kuendigen.betont], ['Passwort ändern', '#passwort', '/datenschutz.html', 'https://webklar.com/impressum', '/kuendigen.html', true])
|
||||||
assert.deepEqual([e.widerrufen.text, e.widerrufen.href, e.widerrufen.pille, e.installieren.text, e.abmelden.text], ['Vertrag widerrufen', '/widerrufen.html', undefined, 'App installieren', 'Abmelden'])
|
assert.deepEqual([e.widerrufen.text, e.widerrufen.href, e.widerrufen.pille, e.installieren.text, e.abmelden.text], ['Vertrag widerrufen', '/widerrufen.html', undefined, 'App installieren', 'Abmelden'])
|
||||||
@@ -37,12 +38,13 @@ test('baueKonto: Kopf als Text, Sheet-Verhalten, Overlay-Kopplung, Navigation, A
|
|||||||
const k = baueKonto({ me, fristen: null, install: null, onAbmelden: () => log.push('abmelden'), onInstall() {}, overlay: { oeffne: (o) => log.push(['overlay', typeof o.schliesse]), beende: () => log.push('beende') }, navigiere: (hash) => log.push(['geheZu', hash]) })
|
const k = baueKonto({ me, fristen: null, install: null, onAbmelden: () => log.push('abmelden'), onInstall() {}, overlay: { oeffne: (o) => log.push(['overlay', typeof o.schliesse]), beende: () => log.push('beende') }, navigiere: (hash) => log.push(['geheZu', hash]) })
|
||||||
fx.document.body.append(k.wurzel); const zeilen = () => fx.alle(k.wurzel, '.kn-konto-zeile')
|
fx.document.body.append(k.wurzel); const zeilen = () => fx.alle(k.wurzel, '.kn-konto-zeile')
|
||||||
assert.deepEqual([fx.finde(k.wurzel, '.kn-konto-name').textContent, fx.finde(k.wurzel, '.kn-konto-mail').textContent], ['<img src=x onerror=alert(1)>Salon', 'a@b.de'])
|
assert.deepEqual([fx.finde(k.wurzel, '.kn-konto-name').textContent, fx.finde(k.wurzel, '.kn-konto-mail').textContent], ['<img src=x onerror=alert(1)>Salon', 'a@b.de'])
|
||||||
assert.deepEqual(zeilen().map((z) => z.textContent), ['Passwort ändern', 'Datenschutz', 'Impressum', 'Verträge kündigen', 'Vertrag widerrufen', 'Abmelden'])
|
assert.deepEqual(zeilen().map((z) => z.textContent), ['Passwort ändern', 'Verträge kündigen', 'Vertrag widerrufen', 'Abmelden', 'Datenschutz', 'Impressum'])
|
||||||
|
assert.deepEqual(zeilen().filter((z) => z.classList.contains('kn-konto-zeile--klein')).map((z) => z.textContent), ['Datenschutz', 'Impressum'])
|
||||||
assert.deepEqual([k.wurzel.getAttribute('role'), k.wurzel.getAttribute('aria-modal')], ['dialog', 'true'])
|
assert.deepEqual([k.wurzel.getAttribute('role'), k.wurzel.getAttribute('aria-modal')], ['dialog', 'true'])
|
||||||
k.oeffne(trigger); assert.deepEqual([k.istOffen(), trigger.getAttribute('aria-expanded'), log[0]], [true, 'true', ['overlay', 'function']])
|
k.oeffne(trigger); assert.deepEqual([k.istOffen(), trigger.getAttribute('aria-expanded'), log[0]], [true, 'true', ['overlay', 'function']])
|
||||||
await fx.sende(k.wurzel, 'keydown', { key: 'Escape' }); assert.deepEqual([k.istOffen(), log.at(-1), fx.document.activeElement === trigger], [false, 'beende', true])
|
await fx.sende(k.wurzel, 'keydown', { key: 'Escape' }); assert.deepEqual([k.istOffen(), log.at(-1), fx.document.activeElement === trigger], [false, 'beende', true])
|
||||||
k.oeffne(trigger); await fx.klick(zeilen()[0]); assert.deepEqual(log.at(-1), ['geheZu', '#passwort'])
|
k.oeffne(trigger); await fx.klick(zeilen()[0]); assert.deepEqual(log.at(-1), ['geheZu', '#passwort'])
|
||||||
await fx.klick(zeilen().at(-1)); assert.equal(log.at(-1), 'abmelden')
|
await fx.klick(zeilen().find((z) => z.getAttribute('data-konto') === 'abmelden')); assert.equal(log.at(-1), 'abmelden')
|
||||||
k.aktualisiere({ fristen: { anzeigen: true }, install: 'ios' }); assert.ok(k.istOffen()); assert.match(fx.text(k.wurzel), /App installieren[\s\S]*Vertrag widerrufen.*Frist läuft/)
|
k.aktualisiere({ fristen: { anzeigen: true }, install: 'ios' }); assert.ok(k.istOffen()); assert.match(fx.text(k.wurzel), /App installieren[\s\S]*Vertrag widerrufen.*Frist läuft/)
|
||||||
await fx.klick(fx.finde(k.wurzel, '.kn-konto-scrim')); assert.equal(k.istOffen(), false)
|
await fx.klick(fx.finde(k.wurzel, '.kn-konto-scrim')); assert.equal(k.istOffen(), false)
|
||||||
} finally { fx.restore() }
|
} finally { fx.restore() }
|
||||||
@@ -62,7 +64,8 @@ test('Konto: Router schließt ohne zweites Zurück, Links öffnen richtig, Tab b
|
|||||||
overlay: { oeffne: (o) => { vomRouter = o.schliesse }, beende: () => log.push('beende') }, navigiere() {} })
|
overlay: { oeffne: (o) => { vomRouter = o.schliesse }, beende: () => log.push('beende') }, navigiere() {} })
|
||||||
fx.document.body.append(k.wurzel); const zeilen = () => fx.alle(k.wurzel, '.kn-konto-zeile')
|
fx.document.body.append(k.wurzel); const zeilen = () => fx.alle(k.wurzel, '.kn-konto-zeile')
|
||||||
assert.equal(fx.finde(k.wurzel, '.kn-konto-name').textContent, 'a@b.de') // ohne Firma und Namen die E-Mail
|
assert.equal(fx.finde(k.wurzel, '.kn-konto-name').textContent, 'a@b.de') // ohne Firma und Namen die E-Mail
|
||||||
const [pw, ds, im, inst] = zeilen()
|
const zeile = (id) => zeilen().find((z) => z.getAttribute('data-konto') === id)
|
||||||
|
const [pw, ds, im, inst] = ['passwort', 'datenschutz', 'impressum', 'installieren'].map(zeile)
|
||||||
assert.deepEqual([ds.getAttribute('target'), ds.getAttribute('rel'), im.getAttribute('target'), pw.hasAttribute('target')], ['_blank', 'noopener', '_blank', false])
|
assert.deepEqual([ds.getAttribute('target'), ds.getAttribute('rel'), im.getAttribute('target'), pw.hasAttribute('target')], ['_blank', 'noopener', '_blank', false])
|
||||||
assert.match(inst.textContent, /Tippe auf Teilen und dann auf Zum Home-Bildschirm\./)
|
assert.match(inst.textContent, /Tippe auf Teilen und dann auf Zum Home-Bildschirm\./)
|
||||||
k.oeffne(trigger); assert.equal(fx.document.activeElement, pw) // Fokus auf der ersten Zeile
|
k.oeffne(trigger); assert.equal(fx.document.activeElement, pw) // Fokus auf der ersten Zeile
|
||||||
|
|||||||
@@ -36,12 +36,16 @@ test('tastaturZustand: öffnen unter voll-150, Android-Geste ohne Blur, Drehen,
|
|||||||
test('huelle.css: Desktop ist Grundregel, Handy überschreibt (Abfrage wörtlich, 44 px, Safe-Area, Reel, Gleiter, Menü, Bewegung)', () => {
|
test('huelle.css: Desktop ist Grundregel, Handy überschreibt (Abfrage wörtlich, 44 px, Safe-Area, Reel, Gleiter, Menü, Bewegung)', () => {
|
||||||
assert.equal(`@media ${HANDY_ABFRAGE}`, HANDY)
|
assert.equal(`@media ${HANDY_ABFRAGE}`, HANDY)
|
||||||
const css = lies('huelle.css'); const handy = handyTeil(css)
|
const css = lies('huelle.css'); const handy = handyTeil(css)
|
||||||
assert.match(css, /\.kn-t\s*\{(?=[^}]*min-height:\s*44px)(?=[^}]*font-size:\s*15px)(?=[^}]*font-weight:\s*600)/s)
|
assert.match(css, /\.kn-t\s*\{(?=[^}]*min-height:\s*44px)(?=[^}]*font-size:\s*15px)(?=[^}]*font-weight:\s*500)/s)
|
||||||
|
assert.match(css, /\.kn-t\[aria-selected="true"\]\s*\{(?=[^}]*color:\s*var\(--kn-text\))(?=[^}]*font-weight:\s*600)/s) // Navigation leichter V3: aktives Wort dunkel und fett
|
||||||
assert.match(css, /\.kn-schrift-klein \.kn-t\s*\{[^}]*font-size:\s*14px/s)
|
assert.match(css, /\.kn-schrift-klein \.kn-t\s*\{[^}]*font-size:\s*14px/s)
|
||||||
assert.match(css, /\.kn-menue\s*\{(?=[^}]*width:\s*var\(--kn-sidebar-w\))(?=[^}]*border-right:\s*1px solid var\(--kn-line\))/s)
|
assert.match(css, /\.kn-menue\s*\{(?=[^}]*width:\s*var\(--kn-sidebar-w\))(?=[^}]*border-right:\s*1px solid var\(--kn-line\))/s)
|
||||||
assert.match(handy, /\.kn-menue\s*\{[^}]*display:\s*none/s); assert.match(handy, /\.kn-leiste\s*\{[^}]*padding-bottom:\s*env\(safe-area-inset-bottom\)/s)
|
assert.match(handy, /\.kn-menue\s*\{[^}]*display:\s*none/s); assert.match(handy, /\.kn-leiste\s*\{[^}]*padding-bottom:\s*env\(safe-area-inset-bottom\)/s)
|
||||||
assert.match(css, /\[data-etage="offen"\][^{]*\{[^}]*translateY\(-56px\)/s)
|
assert.match(css, /\[data-etage="offen"\][^{]*\{[^}]*translateY\(-56px\)/s)
|
||||||
assert.match(css, /\.kn-gleiter\s*\{(?=[^}]*top:\s*4px)(?=[^}]*height:\s*36px)(?=[^}]*border-radius:\s*6px)(?=[^}]*background:\s*var\(--kn-accent\))/s)
|
// Navigation leichter V3: der Gleiter ist eine dünne Linie unter dem Wort, die weiße Overlay-Kopie wird nicht gezeigt
|
||||||
|
assert.match(css, /\.kn-gleiter\s*\{(?=[^}]*bottom:\s*2px)(?=[^}]*height:\s*2px)(?=[^}]*background:\s*var\(--kn-accent\))/s)
|
||||||
|
assert.match(css, /\.kn-overlay\s*\{[^}]*display:\s*none/s)
|
||||||
|
assert.doesNotMatch(css, /\.kn-t\[aria-selected="true"\]\s*\{[^}]*#fff/s) // das aktive Wort ist nie mehr weiß
|
||||||
assert.match(css, /\.kn-tastatur \.kn-leiste[^{]*\{[^}]*(translateY\(110%\)|display:\s*none)/s)
|
assert.match(css, /\.kn-tastatur \.kn-leiste[^{]*\{[^}]*(translateY\(110%\)|display:\s*none)/s)
|
||||||
assert.match(css, /@media \(prefers-reduced-motion: reduce\)[\s\S]*150ms/)
|
assert.match(css, /@media \(prefers-reduced-motion: reduce\)[\s\S]*150ms/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const FAELLE = [ // Alte Links (Review Focus 1)
|
|||||||
['#news?bestellt=1', { bereich: 'funktionen', etage: 'news', query: 'bestellt=1' }],
|
['#news?bestellt=1', { bereich: 'funktionen', etage: 'news', query: 'bestellt=1' }],
|
||||||
['#funktionen', { bereich: 'funktionen', reiter: 'meine', query: '' }], ['#entdecken', { bereich: 'funktionen', reiter: 'entdecken', query: '' }],
|
['#funktionen', { bereich: 'funktionen', reiter: 'meine', query: '' }], ['#entdecken', { bereich: 'funktionen', reiter: 'entdecken', query: '' }],
|
||||||
['#rechnungen', { bereich: 'vertraege', reiter: 'rechnungen', query: '' }], ['#stunden', { bereich: 'vertraege', reiter: 'stunden', query: '' }],
|
['#rechnungen', { bereich: 'vertraege', reiter: 'rechnungen', query: '' }], ['#stunden', { bereich: 'vertraege', reiter: 'stunden', query: '' }],
|
||||||
['#speicher', { bereich: 'vertraege', reiter: 'email', query: '' }], ['#gutschrift', { bereich: 'vertraege', etage: 'gutschrift', query: '' }],
|
['#speicher', { bereich: 'vertraege', etage: 'speicher', query: '' }], // Navigation leichter V7: Unterseite statt Reiter E-Mail
|
||||||
|
['#gutschrift', { bereich: 'vertraege', etage: 'gutschrift', query: '' }],
|
||||||
['#wartung', { bereich: 'vertraege', reiter: 'abo', query: '' }], ['#abo', { bereich: 'vertraege', reiter: 'abo', query: '' }], ['#vertraege', { bereich: 'vertraege', query: '' }], // #vertraege ohne Reiter: die Ansicht wählt den Standardreiter
|
['#wartung', { bereich: 'vertraege', reiter: 'abo', query: '' }], ['#abo', { bereich: 'vertraege', reiter: 'abo', query: '' }], ['#vertraege', { bereich: 'vertraege', query: '' }], // #vertraege ohne Reiter: die Ansicht wählt den Standardreiter
|
||||||
['#fortschritt', { bereich: 'website', scroll: 'aktivitaet', query: '' }], ['#projekt?id=p1', { bereich: 'website', etage: 'projekt', query: 'id=p1' }],
|
['#fortschritt', { bereich: 'website', scroll: 'aktivitaet', query: '' }], ['#projekt?id=p1', { bereich: 'website', etage: 'projekt', query: 'id=p1' }],
|
||||||
['#Chat', { bereich: 'chat', query: '' }], ['#passwort', { bereich: 'website', etage: 'passwort', query: '' }],
|
['#Chat', { bereich: 'chat', query: '' }], ['#passwort', { bereich: 'website', etage: 'passwort', query: '' }],
|
||||||
@@ -69,5 +70,5 @@ test('zeigtWiderrufsstreifen: nicht auf Abo bestellen, Wechsel, Zusatzspeicher u
|
|||||||
assert.equal(zeigtWiderrufsstreifen(undefined), true) // ohne Route nichts verstecken
|
assert.equal(zeigtWiderrufsstreifen(undefined), true) // ohne Route nichts verstecken
|
||||||
// jede Etage der Tabelle ist entschieden: versteckt sind genau die Bestellseiten
|
// jede Etage der Tabelle ist entschieden: versteckt sind genau die Bestellseiten
|
||||||
const versteckt = [...new Set(Object.values(ROUTEN).filter((r) => !zeigtWiderrufsstreifen(r)).map((r) => r.etage ?? `reiter:${r.reiter}`))].sort()
|
const versteckt = [...new Set(Object.values(ROUTEN).filter((r) => !zeigtWiderrufsstreifen(r)).map((r) => r.etage ?? `reiter:${r.reiter}`))].sort()
|
||||||
assert.deepEqual(versteckt, ['bestellen', 'gutschrift', 'reiter:email', 'wechsel'])
|
assert.deepEqual(versteckt, ['bestellen', 'gutschrift', 'speicher', 'wechsel'])
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user