diff --git a/Changes.md b/Changes.md index 79a0849..67e0414 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,33 @@ # Änderungen +## Berichte: Tabellen so breit wie nötig, Zahlen rechtsbündig (18.08.2026) + +Ein Bericht mit zwei oder vier Spalten wurde bisher über die gesamte Fensterbreite gezogen – +zwischen Bezeichnung und Betrag klaffte ein leeres Feld, und das Auge musste die Zeile +zweimal überqueren. Die **Berichtstabellen nehmen jetzt nur den Platz, den ihre Spalten +brauchen** (`width: auto`) und stehen dabei mittig unter der Überschrift; erst wenn der +Inhalt mehr verlangt, wachsen sie bis zur vollen Breite. Im Scrollrahmen `.breittabelle` (zwölf Monatsspalten) bleibt es beim +bisherigen Verhalten: Die Tabelle darf hinausragen und waagerecht scrollen, statt umzubrechen. + +Dabei fiel auf, dass die Zahlenspalten **gar nicht rechtsbündig** standen: `.rechts` und +`.betrag` sind nur einfach spezifisch, `table.liste th, table.liste td { text-align: left }` +weiter oben im Stilblatt ist spezifischer und gewann. Solange die Spalten über den Bildschirm +gezogen waren, fiel es kaum auf – in der schmalen Tabelle sofort. Die Ausrichtung steht jetzt +für **alle Listen** ausdrücklich (`table.liste th.rechts, table.liste td.rechts`): Beträge, +Salden, Stückzahlen und Aktionsspalten stehen überall rechts, wo `.rechts` gesetzt ist – +Kontenübersicht, Kontoblatt, Wertpapiere, Depotbuchungen, Kategorien und die Berichte +gleichermaßen. Die Klasse tut damit endlich das, was ihr Name sagt. + +Der neue E2E-Test misst alles drei: dass der Summenbericht deutlich schmaler bleibt als sein +Rahmen und links wie rechts gleich viel Luft behält, dass die letzte Spalte +`text-align: right` hat und dass die Monatsansicht weiterhin breiter wird. + +Nachgezogen: Der E2E-Test der eingeklappten Seitenleiste erwartete `toBeHidden()` für die +Beschriftungen. Die sind aber mit Absicht nur aus dem Bild geschoben (`clip-path`, 1×1 Pixel) +statt entfernt, damit Vorleseprogramme die Namen behalten – für Playwright bleibt so ein +Element „sichtbar“. Geprüft wird jetzt, dass die Beschriftung keinen Platz mehr einnimmt und +der Verweis trotzdem seinen zugänglichen Namen behält. + ## Seitenleiste: Marke mit Münzstapel, ein Klick schiebt sie auf Symbolbreite (18.08.2026) Oben in der Seitenleiste steht jetzt eine richtige **Marke**: das Bild `Coinstack.png` aus den diff --git a/public/static/app.css b/public/static/app.css index 63ad251..f091782 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -762,6 +762,13 @@ table.liste tbody tr:hover { background: var(--flaeche-gedaempft); } .rechts { text-align: right; } +/* In Listen reicht `.rechts` allein nicht: `table.liste th, table.liste td` + weiter oben setzt text-align auf links und ist spezifischer. Zahlen- und + Aktionsspalten stehen deshalb ausdrücklich rechts – die Klasse tut damit + überall das, was ihr Name sagt. */ +table.liste th.rechts, +table.liste td.rechts { text-align: right; } + .betrag { font-family: var(--schrift-zahl); font-variant-numeric: tabular-nums; @@ -1737,6 +1744,20 @@ body.schlicht { table.bericht { font-size: .9rem; } table.bericht td { padding: .28rem .7rem; } +/* Ein Bericht mit wenigen Spalten soll nicht über die ganze Breite gezogen + werden: Die Tabelle nimmt nur den Platz, den ihre Spalten brauchen, und + steht dabei mittig unter der Überschrift. Die Zahlenspalten bleiben in sich + rechtsbündig (.rechts), die letzte schließt bündig mit dem Tabellenrand ab. */ +table.liste.bericht { + width: auto; + max-width: 100%; + margin-inline: auto; +} + +/* Im Scrollrahmen darf die Tabelle über die sichtbare Breite hinausgehen – + sonst quetschte max-width die zwölf Monatsspalten in Umbrüche. */ +.breittabelle table.liste.bericht { max-width: none; } + /* Abschnittsüberschrift „Einnahmen“ / „Ausgaben“. */ table.bericht tr.abschnittszeile th { text-align: left; diff --git a/tests/e2e/specs/auswertungen.spec.js b/tests/e2e/specs/auswertungen.spec.js index 2a8c7a6..769993f 100644 --- a/tests/e2e/specs/auswertungen.spec.js +++ b/tests/e2e/specs/auswertungen.spec.js @@ -145,6 +145,38 @@ test.describe('Auswertungen', () => { await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht') }) + test('Schmale Berichte bleiben schmal, die Zahlenspalte bleibt rechtsbündig', async ({ page }) => { + // Zwei Spalten brauchen keine Bildschirmbreite: Die Tabelle steht links + // und ist deutlich schmaler als der Platz, den sie hätte. + await vorlage(page, 'summenbericht') + + const tabelle = page.locator('table.bericht') + const rahmen = page.locator('.breittabelle') + + const tabelleKasten = await tabelle.boundingBox() + const rahmenKasten = await rahmen.boundingBox() + const tabelleBreit = tabelleKasten.width + expect(tabelleBreit).toBeLessThan(rahmenKasten.width * 0.9) + + // … und steht dabei mittig: links und rechts bleibt gleich viel Luft. + const links = tabelleKasten.x - rahmenKasten.x + const rechts = (rahmenKasten.x + rahmenKasten.width) - (tabelleKasten.x + tabelleBreit) + expect(Math.abs(links - rechts)).toBeLessThanOrEqual(1) + + // Letzte Spalte: Zahlen rechtsbündig in ihrer eigenen Spalte. + // Die erste Zeile eines Abschnitts ist seine Überschrift (th) – deshalb + // die erste echte Wertzeile. + const letzte = tabelle.locator('tbody tr > td:last-child').first() + await expect(letzte).toHaveCSS('text-align', 'right') + + // Zwölf Monatsspalten dürfen dagegen weiterhin über die Breite gehen + // und im Rahmen scrollen, statt umzubrechen. + await page.selectOption('select[name="aufteilung"]', 'monat') + await page.click('button:has-text("Anzeigen")') + expect((await tabelle.boundingBox()).width) + .toBeGreaterThan(tabelleBreit) + }) + test('CSV-Ausgabe kommt als Datei mit deutschem Zahlenformat', async ({ page }) => { await vorlage(page, 'summenbericht') diff --git a/tests/e2e/specs/module.spec.js b/tests/e2e/specs/module.spec.js index 1356400..bc4f1b3 100644 --- a/tests/e2e/specs/module.spec.js +++ b/tests/e2e/specs/module.spec.js @@ -54,6 +54,25 @@ test.describe('Module der Phasen 4 bis 7', () => { } }) + test('Zahlenspalten stehen in allen Listen rechtsbündig', async ({ page }) => { + // `.rechts` markiert Zahlen- und Aktionsspalten. Die Klasse muss die + // linksbündige Grundregel der Listen überall schlagen – nicht nur in den + // Berichten, wo es zuerst auffiel. + for (const pfad of ['/konten', '/wertpapiere', '/wertpapiere/buchungen']) { + await page.goto(pfad) + + for (const art of ['th', 'td']) { + const zellen = page.locator(`table.liste ${art}.rechts`) + const anzahl = await zellen.count() + expect(anzahl, `${pfad}: ${art}.rechts vorhanden`).toBeGreaterThan(0) + + for (const stelle of [0, anzahl - 1]) { + await expect(zellen.nth(stelle)).toHaveCSS('text-align', 'right') + } + } + } + }) + test('Das Cockpit zeichnet seine Kacheln', async ({ page }) => { await page.goto('/') diff --git a/tests/e2e/specs/seitenleiste.spec.js b/tests/e2e/specs/seitenleiste.spec.js index 89bfe34..72b8a66 100644 --- a/tests/e2e/specs/seitenleiste.spec.js +++ b/tests/e2e/specs/seitenleiste.spec.js @@ -14,6 +14,19 @@ async function anmelden(page) { await expect(page).toHaveURL(/\/$/) } +/** + * Eingeklappt werden die Beschriftungen nur aus dem Bild geschoben (`clip-path` + * auf 1×1 Pixel), nicht mit `display:none` entfernt – sonst verlören die + * Verweise ihren Namen für Vorleseprogramme. Für Playwright bleibt ein solches + * Element „sichtbar“; geprüft wird deshalb, dass es keinen Platz mehr einnimmt. + */ +async function istWeggeschoben(beschriftung) { + await expect.poll(async () => { + const kasten = await beschriftung.boundingBox() + return kasten === null ? 0 : Math.max(kasten.width, kasten.height) + }, { message: 'Beschriftung nimmt keinen Platz mehr ein' }).toBeLessThanOrEqual(1) +} + test.describe('Seitenleiste ein- und ausklappen', () => { test('Klick auf die Marke schiebt die Leiste auf Symbolbreite und zurück', async ({ page }) => { await anmelden(page) @@ -26,6 +39,7 @@ test.describe('Seitenleiste ein- und ausklappen', () => { await expect(symbol).toBeVisible() await expect(beschriftung).toBeVisible() const breitOffen = (await page.locator('.seitenleiste').boundingBox()).width + const hauptbereichOffen = (await page.locator('.hauptbereich').boundingBox()).width await marke.click() @@ -34,21 +48,25 @@ test.describe('Seitenleiste ein- und ausklappen', () => { // Das Markensymbol und die Symbole der Menüpunkte bleiben stehen, // nur die Beschriftungen verschwinden. await expect(symbol).toBeVisible() - await expect(beschriftung).toBeHidden() + await istWeggeschoben(beschriftung) await expect(page.locator('.seitenleiste .icon').first()).toBeVisible() + // Der Verweis behält dabei seinen Namen – nur das Auge verliert ihn. + await expect(page.locator('.seitenleiste a[href="/konten"]')) + .toHaveAccessibleName(/Kontenübersicht/) + const breitEingeklappt = (await page.locator('.seitenleiste').boundingBox()).width expect(breitEingeklappt).toBeLessThan(breitOffen) // Der Hauptbereich bekommt den gewonnenen Platz. - const hauptOffen = breitOffen - expect((await page.locator('.hauptbereich').boundingBox()).width).toBeGreaterThan(0) + expect((await page.locator('.hauptbereich').boundingBox()).width) + .toBeGreaterThan(hauptbereichOffen) await marke.click() await expect(rahmen).not.toHaveClass(/eingeklappt/) await expect(marke).toHaveAttribute('aria-expanded', 'true') await expect(beschriftung).toBeVisible() - expect((await page.locator('.seitenleiste').boundingBox()).width).toBe(hauptOffen) + expect((await page.locator('.seitenleiste').boundingBox()).width).toBe(breitOffen) }) test('Die Symbole führen eingeklappt zu denselben Seiten', async ({ page }) => { @@ -64,7 +82,7 @@ test.describe('Seitenleiste ein- und ausklappen', () => { // Der Zustand hält über den Seitenwechsel hinweg – ohne Aufblitzen, // weil der Server ihn schon aus dem Cookie kennt. await expect(page.locator('.rahmen')).toHaveClass(/eingeklappt/) - await expect(page.locator('.seitenleiste .beschriftung').first()).toBeHidden() + await istWeggeschoben(page.locator('.seitenleiste .beschriftung').first()) await page.locator('[data-seitenleiste-schalter]').click() await expect(page.locator('.rahmen')).not.toHaveClass(/eingeklappt/)