Barrierefreiheit und mobile Darstellung

- Kontrast: gedämpfter Text war mit 3,1:1 unter WCAG AA, jetzt 4,9:1
  (hell) bzw. 6,4:1 (dunkel)
- Sprungmarke „Zum Inhalt springen" in Admin-Shell und Voucher-Seite
- aria-label für alle reinen Icon-Schaltflächen (Theme, Menü, Abmelden,
  Zeilenaktionen), aria-current auf dem aktiven Navigationspunkt,
  role="group" für den Sprachumschalter
- 194 dekorative Icons mit aria-hidden versehen, damit Screenreader sie
  nicht vorlesen
- Toast-Container als aria-live-Bereich ausgezeichnet
- prefers-reduced-motion schaltet Animationen und Übergänge ab

Tabellen (Benutzer, Vouchers, Profile, Audit-Log, Dashboard, API-Keys)
werden unter 720 px zu Karten: die Spaltenüberschrift steht per
data-label vor dem Wert, statt horizontal zu scrollen.

Datums- und Zeitformat in der Voucher-Liste folgen jetzt der gewählten
Sprache statt fest de-DE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 06:38:08 +00:00
parent 850a04d628
commit 7f1d93debd
21 changed files with 716 additions and 632 deletions

View file

@ -308,43 +308,44 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</head>
<body class="app-body">
<a class="skip-link no-print" href="#main-content"><?= __('a11y_skip') ?></a>
<header class="app-topbar no-print">
<a href="index.php" class="brand">
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
<span class="brand-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
<span class="brand-name"><?= htmlspecialchars($appTitle) ?></span>
</a>
<div class="app-actions">
<div class="lang-switcher">
<div class="lang-switcher" role="group" aria-label="<?= __('a11y_language') ?>">
<?php foreach (I18n::getAvailable() as $code => $label): ?>
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
<?php endforeach; ?>
</div>
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
<i class="fas fa-moon"></i>
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" aria-label="<?= __('a11y_theme') ?>" title="<?= __('a11y_theme') ?>">
<i class="fas fa-moon" aria-hidden="true"></i>
</button>
<?php if ($currentUser): ?>
<?php if ($auth->isAdmin()): ?>
<a href="admin/" class="btn btn-secondary">
<i class="fas fa-sliders"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
<i class="fas fa-sliders" aria-hidden="true"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
</a>
<?php endif; ?>
<div class="user-menu">
<div class="user-avatar"><?= strtoupper(mb_substr($currentUser['name'], 0, 1)) ?></div>
<div class="user-name hide-mobile"><?= htmlspecialchars($currentUser['name']) ?></div>
<a href="logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" style="width:28px;height:28px;font-size:11px;">
<i class="fas fa-arrow-right-from-bracket"></i>
<a href="logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" aria-label="<?= __('btn_logout') ?>" style="width:28px;height:28px;font-size:11px;">
<i class="fas fa-arrow-right-from-bracket" aria-hidden="true"></i>
</a>
</div>
<?php elseif ($publicAccess): ?>
<a href="login.php" class="btn btn-secondary">
<i class="fas fa-right-to-bracket"></i> <?= __('btn_login') ?>
<i class="fas fa-right-to-bracket" aria-hidden="true"></i> <?= __('btn_login') ?>
</a>
<?php endif; ?>
</div>
</header>
<div class="container">
<main class="container" id="main-content">
<?php if ($logoUrl && !$voucherCreated && !$bulkCreated): ?>
<img src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="Logo" class="logo">
<?php endif; ?>
@ -366,17 +367,17 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</div>
<div class="voucher-result no-print">
<div class="result-label"><i class="fas fa-circle-check"></i> <?= __('voucher_success_title') ?></div>
<div class="result-label"><i class="fas fa-circle-check" aria-hidden="true"></i> <?= __('voucher_success_title') ?></div>
<div class="voucher-code" id="voucherCode" onclick="copyCode()" title="<?= __('js_click_to_copy') ?>">
<?= htmlspecialchars($voucherCode) ?>
</div>
<div class="voucher-info">
<i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?>
<i class="fas fa-copy" aria-hidden="true"></i> <?= __('voucher_copy_hint') ?>
</div>
<div class="voucher-meta">
<span><i class="fas fa-clock"></i> <?= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?></span>
<span><i class="fas fa-location-dot"></i> <?= htmlspecialchars($voucherData['site_name']) ?></span>
<span><i class="fas fa-mobile-screen"></i> <?= (int)$voucherData['max_uses'] ?> <?= __('label_devices') ?></span>
<span><i class="fas fa-clock" aria-hidden="true"></i> <?= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?></span>
<span><i class="fas fa-location-dot" aria-hidden="true"></i> <?= htmlspecialchars($voucherData['site_name']) ?></span>
<span><i class="fas fa-mobile-screen" aria-hidden="true"></i> <?= (int)$voucherData['max_uses'] ?> <?= __('label_devices') ?></span>
</div>
<div class="ticket-divider"></div>
<div class="qr-wrapper no-print">
@ -395,11 +396,11 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<div class="no-print" style="display:flex;gap:10px;margin-top:18px;">
<form method="get" style="flex:1;">
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-plus"></i> <?= __('btn_new_code') ?>
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('btn_new_code') ?>
</button>
</form>
<button onclick="window.print()" class="btn btn-secondary btn-lg">
<i class="fas fa-print"></i> <?= __('voucher_print_btn') ?>
<i class="fas fa-print" aria-hidden="true"></i> <?= __('voucher_print_btn') ?>
</button>
</div>
@ -441,12 +442,12 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php endforeach; ?>
</tbody>
</table>
<p class="copy-hint"><i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?></p>
<p class="copy-hint"><i class="fas fa-copy" aria-hidden="true"></i> <?= __('voucher_copy_hint') ?></p>
</div>
<div class="no-print" style="display:flex;gap:10px;margin-top:20px;">
<button onclick="window.print()" class="btn btn-primary btn-lg" style="flex:1;">
<i class="fas fa-print"></i> <?= __('bulk_print_all') ?>
<i class="fas fa-print" aria-hidden="true"></i> <?= __('bulk_print_all') ?>
</button>
<form method="get" style="flex:1;">
<button type="submit" class="btn btn-secondary btn-lg btn-block"><?= __('btn_new_code') ?></button>
@ -455,7 +456,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php elseif (empty($sites)): ?>
<div class="empty-state">
<div class="empty-icon"><i class="fas fa-wifi"></i></div>
<div class="empty-icon"><i class="fas fa-wifi" aria-hidden="true"></i></div>
<p><?= __('voucher_no_sites') ?><br>
<?php if ($auth->isAdmin()): ?>
<a href="admin/" style="color:var(--accent);"><?= __('voucher_no_sites_admin') ?></a>
@ -539,7 +540,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<div class="email-checkbox-wrapper">
<input type="checkbox" id="send_email" name="send_email" onchange="toggleEmailField()">
<label for="send_email">
<i class="fas fa-envelope" style="color:var(--accent);"></i> <?= __('voucher_email_send') ?>
<i class="fas fa-envelope" style="color:var(--accent);" aria-hidden="true"></i> <?= __('voucher_email_send') ?>
</label>
</div>
<div class="email-input-wrapper" id="email_field">
@ -556,7 +557,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<div class="email-option">
<div class="email-checkbox-wrapper">
<input type="checkbox" id="send_sms" name="send_sms" onchange="document.getElementById('sms_field').style.display=this.checked?'block':'none'">
<label for="send_sms"><i class="fas fa-comment-sms" style="color:var(--accent);"></i> Code per SMS versenden</label>
<label for="send_sms"><i class="fas fa-comment-sms" style="color:var(--accent);" aria-hidden="true"></i> Code per SMS versenden</label>
</div>
<div id="sms_field" style="display:none;margin-top:12px;">
<label for="recipient_phone">Telefonnummer (international, z.B. +49170)</label>
@ -566,7 +567,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php endif; ?>
<button type="submit" class="btn btn-primary btn-lg btn-block" id="submitBtn">
<i class="fas fa-ticket"></i> <?= __('voucher_create_btn') ?>
<i class="fas fa-ticket" aria-hidden="true"></i> <?= __('voucher_create_btn') ?>
</button>
</form>
</div>
@ -629,9 +630,9 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php endif; ?>
<?php endif; ?>
</div>
</main>
<div id="toast-container"></div>
<div id="toast-container" role="status" aria-live="polite"></div>
<script src="assets/global.js"></script>
<script>
<?php if ($voucherCreated): ?>
@ -711,7 +712,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
const btn = document.getElementById('submitBtn');
if (!btn || btn.disabled) { e.preventDefault(); return; }
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= __('voucher_creating') ?>';
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> <?= __('voucher_creating') ?>';
});
document.getElementById('bulkForm')?.addEventListener('submit', function(e) {
@ -719,7 +720,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
if (!btn || btn.disabled) { e.preventDefault(); return; }
const count = document.getElementById('bulk_count').value;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>';
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>';
});
document.addEventListener('DOMContentLoaded', function() {