feat(readability): add scalable text and responsive high-contrast layouts (U13)
This commit is contained in:
@@ -79,3 +79,9 @@ Beide Seiten zeigen dieselbe Richtung Von → Nach und erklären das feste aktue
|
|||||||
## U12 – Anfangsbestand und Bestandskorrektur
|
## U12 – Anfangsbestand und Bestandskorrektur
|
||||||
|
|
||||||
Neue Konten erhalten einen erklärten Anfangsbestand ohne künstliche Einnahme. Spätere Korrektur zeigt Konto/Jahr, Formel, vorher/nachher für gewählten Bestand, gemeinsamen Anfangsbestand und Endstand sowie ausdrücklich die Auswirkungen auf frühere/spätere Jahre. Zustimmung ist erst nach Eingabe gültig, Abbrechen hat Anfangsfokus. Bestehende gemeinsame Semantik bleibt unverändert, keine Migration oder jahresbezogene neue Speicherregel. Dienst prüft den erwarteten Bestand in einer Transaktion und weist veraltete Vorschauen ab. Mehrjahrestest prüft die Verschiebung aller Jahre bei unveränderten Buchungen, Formularprüfung die notwendige Zustimmung.
|
Neue Konten erhalten einen erklärten Anfangsbestand ohne künstliche Einnahme. Spätere Korrektur zeigt Konto/Jahr, Formel, vorher/nachher für gewählten Bestand, gemeinsamen Anfangsbestand und Endstand sowie ausdrücklich die Auswirkungen auf frühere/spätere Jahre. Zustimmung ist erst nach Eingabe gültig, Abbrechen hat Anfangsfokus. Bestehende gemeinsame Semantik bleibt unverändert, keine Migration oder jahresbezogene neue Speicherregel. Dienst prüft den erwarteten Bestand in einer Transaktion und weist veraltete Vorschauen ab. Mehrjahrestest prüft die Verschiebung aller Jahre bei unveränderten Buchungen, Formularprüfung die notwendige Zustimmung.
|
||||||
|
|
||||||
|
## U13 – Lesbarkeit und Fenstergröße
|
||||||
|
|
||||||
|
Grundschrift 16 px, beschriftete Textgrößenwahl bis 200 % (lokal gemerkt), keine kleinen Großbuchstaben in Formular/Tabellenkopf. Lange Namen umbrechen, Dialoge passen ins Fenster und scrollen; bei schmaler Ansicht einspaltige Formulare und Buchungskarten. Licht-/Dunkelrollen für Text und Beträge getrennt; Gelöscht bleibt zusätzlich als Text erkennbar. Keine Änderung von Windows-DPI-Einstellungen und keine daraus abgeleitete vollständige Barrierefreiheitszusage. Kontrastmessung und Layout-Stichproben werden im Abschluss ergänzt.
|
||||||
|
|
||||||
|
U13-Kontrastmessung: Text-, Hilfstext-, Einnahme-, Ausgabe- und Umbuchungsfarben gegen Hintergrund/Oberfläche jeweils mindestens 5.91:1 (hell) und 8.37:1 (dunkel); beschriftete primäre/Erfassen-/Umbuchungs-/Löschbuttons mindestens 6.47:1. Dies sind berechnete Rollen-Stichproben, keine vollständige Messung sämtlicher Zustände.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@inject ToastService Toasts
|
@inject ToastService Toasts
|
||||||
<ModalDialog @ref="modal" Title="Bestandskorrektur" OnCancel="OnCancel" Dirty="@(amount != CurrentAmount)" Busy="saving">
|
<ModalDialog @ref="modal" Title="Bestandskorrektur" OnCancel="OnCancel" Dirty="@(amountText != CurrentAmount.ToString("0.00", System.Globalization.CultureInfo.GetCultureInfo("de-DE")))" Busy="saving">
|
||||||
<h5>Bestandskorrektur – @AccountName</h5>
|
<h5>Bestandskorrektur – @AccountName</h5>
|
||||||
<p>@(Year.HasValue ? $"Übertrag zum 01.01.{Year}" : "Gemeinsamer Anfangsbestand – alle Jahre")</p>
|
<p>@(Year.HasValue ? $"Übertrag zum 01.01.{Year}" : "Gemeinsamer Anfangsbestand – alle Jahre")</p>
|
||||||
<p>Anfangsbestand + Einnahmen − Ausgaben = Kontostand</p>
|
<p>Anfangsbestand + Einnahmen − Ausgaben = Kontostand</p>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
<ToastHost />
|
<ToastHost />
|
||||||
|
|
||||||
<div class="d-flex flex-column vh-100">
|
<div class="app-shell">
|
||||||
<nav class="app-navbar d-flex justify-content-between align-items-center">
|
<nav class="app-navbar d-flex justify-content-between align-items-center">
|
||||||
<a class="navbar-brand" href="">
|
<a class="navbar-brand" href="">
|
||||||
<strong>Dümpelkas</strong> <small class="text-muted">Kassenbuch</small>
|
<strong>Dümpelkas</strong> <small class="text-muted">Kassenbuch</small>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="reading-controls">
|
||||||
|
<label for="text-scale">Textgröße</label>
|
||||||
|
<select id="text-scale" @bind="fontScale" @bind:after="ApplyTextScale">
|
||||||
|
<option value="100">Normal</option><option value="125">Groß (125 %)</option>
|
||||||
|
<option value="150">Sehr groß (150 %)</option><option value="200">200 %</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="flex-grow-1 overflow-auto p-3">
|
<main class="flex-grow-1 overflow-auto p-3">
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Duempelkas.App.Components.Layout;
|
namespace Duempelkas.App.Components.Layout;
|
||||||
|
|
||||||
public partial class MainLayout
|
public partial class MainLayout
|
||||||
{
|
{
|
||||||
|
private int fontScale = 100;
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
var value = await JS.InvokeAsync<int>("duempelkasReading.get");
|
||||||
|
fontScale = value is 125 or 150 or 200 ? value : 100;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private async Task ApplyTextScale() => await JS.InvokeVoidAsync("duempelkasReading.set", fontScale);
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private bool showAboutDialog;
|
private bool showAboutDialog;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{
|
{
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
|
<div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
|
||||||
<h1 class="mb-0 mt-0" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
<h1 class="mb-0 mt-0 page-heading">
|
||||||
@account.Name
|
@account.Name
|
||||||
<button class="btn-edit-pen" @onclick="() => showEditName = true" title="Name bearbeiten"><i class="bi bi-pencil"></i></button>
|
<button class="btn-edit-pen" @onclick="() => showEditName = true" title="Name bearbeiten"><i class="bi bi-pencil"></i></button>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
|
<div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
|
||||||
<h1 class="mb-0 mt-0" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
<h1 class="mb-0 mt-0 page-heading">
|
||||||
@DisplayClubName
|
@DisplayClubName
|
||||||
<button class="btn-edit-pen" @onclick="() => showEditClubName = true" title="Vereinsname bearbeiten">
|
<button class="btn-edit-pen" @onclick="() => showEditClubName = true" title="Vereinsname bearbeiten">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
|
|||||||
@@ -658,3 +658,42 @@ body:has(dialog[open]) .toast-stack:not(.modal-toasts) { display: none; }
|
|||||||
.history-toggle { display: block; padding: .75rem 0; }
|
.history-toggle { display: block; padding: .75rem 0; }
|
||||||
.entry-deleted { opacity: 1 !important; }
|
.entry-deleted { opacity: 1 !important; }
|
||||||
.deleted-status { text-decoration: none; color: var(--color-text-muted); margin: .3rem 0; }
|
.deleted-status { text-decoration: none; color: var(--color-text-muted); margin: .3rem 0; }
|
||||||
|
|
||||||
|
/* Readable theme roles, also used when external styles are unavailable. */
|
||||||
|
:root { --color-bg: #f1f5f9; --color-income: #166534; --color-expense: #b91c1c; --color-transfer: #1d4ed8; --color-text-muted: #475569; --color-border: #94a3b8; }
|
||||||
|
html[data-ui-theme="dark"] { --color-income: #4ade80; --color-expense: #fca5a5; --color-transfer: #93c5fd; --color-text-muted: #cbd5e1; --color-border: #64748b; }
|
||||||
|
html { font-size: var(--app-font-size, 16px); }
|
||||||
|
body { line-height: 1.5; }
|
||||||
|
h1,h2,h3,h4,h5,h6 { font-family: inherit; }
|
||||||
|
small, .text-muted { color: var(--color-text-muted) !important; }
|
||||||
|
small { font-size: .9rem; }
|
||||||
|
.form-label, .table thead th { text-transform: none; letter-spacing: normal; font-size: 1rem; }
|
||||||
|
.app-shell { height: 100dvh; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
|
||||||
|
.app-navbar, .app-footer { flex-wrap: wrap; gap: .5rem; height: auto; min-height: 2.5rem; }
|
||||||
|
.app-footer { font-size: .9rem; }
|
||||||
|
.reading-controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
||||||
|
.reading-controls select { padding: .35rem; color: var(--color-text); background: var(--color-surface); border: 1px solid var(--color-border); }
|
||||||
|
.page-heading, .card-title { overflow-wrap: anywhere; white-space: normal; }
|
||||||
|
.app-modal { box-sizing: border-box; width: min(38rem, calc(100vw - 2rem)); min-width: 0; max-width: calc(100vw - 2rem); max-height: calc(100dvh - 2rem); }
|
||||||
|
.app-modal p, .app-modal label { overflow-wrap: anywhere; }
|
||||||
|
.app-modal .dialog-actions, .app-modal form > .d-flex { flex-wrap: wrap; position: sticky; bottom: -1rem; background: var(--color-surface); padding: .75rem 0; }
|
||||||
|
.form-container { max-width: none; }
|
||||||
|
.btn-primary { background: #4338ca; border-color: #4338ca; color: #fff; }
|
||||||
|
.btn-success { background: #166534; border-color: #166534; color: #fff; }
|
||||||
|
.btn-info { background: #1e40af; border-color: #1e40af; color: #fff; }
|
||||||
|
.btn-danger { background: #b91c1c; border-color: #b91c1c; color: #fff; }
|
||||||
|
.btn-warning { background: #fbbf24; border-color: #fbbf24; color: #111827; }
|
||||||
|
.btn-secondary { background: var(--color-surface-hover); color: var(--color-text); border-color: var(--color-border); }
|
||||||
|
.btn:hover { filter: brightness(.92); }
|
||||||
|
input,select,textarea,button { font: inherit; }
|
||||||
|
button,summary { touch-action: manipulation; }
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.form-container { grid-template-columns: minmax(0, 1fr); gap: .5rem; }
|
||||||
|
.form-container label,.form-container input,.form-container select,.form-container textarea { grid-column: auto; }
|
||||||
|
.action-toolbar { gap: .5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover { background: #4338ca; border-color: #4338ca; color: #fff; }
|
||||||
|
.btn-success:hover { background: #166534; border-color: #166534; color: #fff; }
|
||||||
|
.btn-info:hover { background: #1e40af; border-color: #1e40af; color: #fff; }
|
||||||
|
.btn-danger:hover { background: #b91c1c; border-color: #b91c1c; color: #fff; }
|
||||||
|
|||||||
@@ -30,3 +30,14 @@ window.duempelkasDialog = {
|
|||||||
close(dialog) { if (dialog?.open) dialog.close(); },
|
close(dialog) { if (dialog?.open) dialog.close(); },
|
||||||
focus(dialog, selector) { dialog.querySelector(selector)?.focus(); }
|
focus(dialog, selector) { dialog.querySelector(selector)?.focus(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.duempelkasReading = {
|
||||||
|
get() { let value = 100; try { value = Number(localStorage.getItem('duempelkas-text-scale')) || 100; } catch (_) {} return this.set(value, false); },
|
||||||
|
set(value, persist = true) {
|
||||||
|
const scale = [100,125,150,200].includes(Number(value)) ? Number(value) : 100;
|
||||||
|
document.documentElement.style.setProperty('--app-font-size', (16 * scale / 100) + 'px');
|
||||||
|
if (persist) { try { localStorage.setItem('duempelkas-text-scale', String(scale)); } catch (_) {} }
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.duempelkasReading.get();
|
||||||
|
|||||||
Reference in New Issue
Block a user