fix(a11y): Beschriftungen verknuepfen, Reiter umbrechen, Badge-Farbton zurueck
- Alle 14 <label> ohne Bezug haben jetzt htmlFor/id — davon 9 in den Fuehrer-Formularen, also genau dort, wo am Handy getippt wird. Die GPS-Gruppenueberschrift im Benutzerformular ist keine Feldbeschriftung mehr, sondern eine Ueberschrift. - Das Auf- und Zuklappen im Audit-Log lag auf einem <div> und war mit der Tastatur nicht erreichbar; jetzt ein <button> mit aria-expanded, das bei fehlenden Details disabled ist. .audit-item-header war ausserdem zweimal in derselben Datei definiert — zusammengefasst. - Die Reiter im Admin-Panel standen in einem inline-flex ohne Umbruch: bei 360 px war die Leiste 395 px breit und die Seite lief horizontal ueber. Die Datei hatte gar keine Media-Query. Jetzt Umbruch, ein Breakpoint fuer <=480px und 44 px Mindesthoehe je Reiter. - REGRESSION der letzten Runde: die geskriptete Token-Migration hat #e8eaf6 (Indigo-Toenung) als neutrale Flaeche eingestuft, wodurch .badge-bulk-update im Hellmodus nicht mehr von .badge-default zu unterscheiden war. Farbton zurueckgesetzt. - .settings-section-count und .settings-rule-index nutzten --color-border als Flaeche und lagen bei 4,49:1 — jetzt --color-surface-alt. - Checkbox, Radio und Schieberegler folgen ueber accent-color der Markenfarbe statt dem Browser-Blau. Geprueft: rechnerischer Kontrast-Durchlauf ueber alle Regeln mit Flaeche und Text in beiden Ansichten; kein Ueberlauf mehr bei 360 px in Admin-Panel und Fuehrer-Dashboard, hell wie dunkel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
87011eee3c
commit
e2af87a7e5
|
|
@ -13,6 +13,11 @@
|
|||
in beiden Varianten nachgerechnet.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
/* Sagt dem Browser, dass die Seite beide Ansichten kann. Native Bedienelemente
|
||||
— Checkboxen, Auswahllisten, Bildlaufleisten — passen sich dadurch an,
|
||||
statt in der Nachtansicht hell aufzublitzen. */
|
||||
color-scheme: light dark;
|
||||
|
||||
--font-display: Georgia, 'Times New Roman', serif;
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
|
@ -159,9 +164,6 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
/* Damit auch vom Browser gestellte Bedienelemente (Bildlaufleisten,
|
||||
Datumsauswahl, Autofill) der gewählten Ansicht folgen. */
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Formularfelder brauchen ausdrücklich Farben: ohne sie nimmt der Browser
|
||||
|
|
@ -179,6 +181,13 @@ textarea::placeholder {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Checkbox, Radio und Schieberegler in der Markenfarbe statt im Browser-Blau. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
input[type="range"] {
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
.admin-tabs {
|
||||
display: inline-flex;
|
||||
/* Vier Reiter passen auf schmalen Displays nicht in eine Zeile — ohne
|
||||
Umbruch lief die Seite bei 360 px horizontal ueber. */
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -14,6 +17,7 @@
|
|||
}
|
||||
|
||||
.tab-button {
|
||||
min-height: var(--touch-target);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
|
@ -122,7 +126,7 @@
|
|||
.settings-section-count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 999px;
|
||||
padding: 0.1rem 0.5rem;
|
||||
}
|
||||
|
|
@ -164,7 +168,7 @@
|
|||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -346,3 +350,16 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1 1 45%;
|
||||
padding: 0.5rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,8 +371,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
</div>
|
||||
<div className="settings-section-body">
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input
|
||||
<label className="settings-label" htmlFor="settings-appname">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input id="settings-appname"
|
||||
type="text"
|
||||
className="settings-input"
|
||||
value={config.appName || ''}
|
||||
|
|
@ -439,8 +439,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
{ key: 'ansprechpartner', label: 'Ansprechpartner und Koordination' }
|
||||
].map(({ key, label }) => (
|
||||
<div key={key} className="settings-field">
|
||||
<label className="settings-label">{label}</label>
|
||||
<textarea
|
||||
<label className="settings-label" htmlFor={`settings-section-${key}`}>{label}</label>
|
||||
<textarea id={`settings-section-${key}`}
|
||||
value={getSectionContent(key)}
|
||||
onChange={(e) => updateSection(key, e.target.value)}
|
||||
className="settings-textarea settings-textarea-lg"
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
.badge-login-failed { background: #ffcdd2; color: #b71c1c; font-weight: 700; }
|
||||
.badge-import { background: #e0f7fa; color: #00695c; }
|
||||
.badge-export { background: #e8f5e9; color: #1b5e20; }
|
||||
.badge-bulk-update { background: var(--color-surface-alt); color: #283593; }
|
||||
.badge-bulk-update { background: #e8eaf6; color: #283593; }
|
||||
.badge-bulk-delete { background: #fbe9e7; color: #bf360c; }
|
||||
.badge-password { background: #fff8e1; color: #9c4e00; }
|
||||
.badge-default { background: #f5f5f5; color: #616161; }
|
||||
|
|
@ -280,14 +280,25 @@
|
|||
.audit-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.audit-item-failed { border-left: 4px solid var(--color-danger); }
|
||||
|
||||
/* Als <button> ausgezeichnet, damit das Auf- und Zuklappen auch mit der
|
||||
Tastatur erreichbar ist. Die Button-Grundstile werden hier zurueckgesetzt. */
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: var(--color-surface-alt);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.audit-item-header:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.audit-time { margin-left: auto; color: var(--color-text-muted); font-size: 12px; white-space: nowrap; }
|
||||
.expand-toggle { color: var(--color-text-muted); font-size: 11px; cursor: pointer; padding: 0 4px; }
|
||||
|
|
@ -502,13 +513,6 @@
|
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.audit-badge {
|
||||
padding: 6px 12px;
|
||||
|
|
|
|||
|
|
@ -228,8 +228,13 @@ function AuditLogs() {
|
|||
|
||||
return (
|
||||
<div key={log._id} className={`audit-item ${!log.success ? 'audit-item-failed' : ''}`}>
|
||||
<div className="audit-item-header" onClick={() => hasDetail && toggleExpand(log._id)}
|
||||
style={{ cursor: hasDetail ? 'pointer' : 'default' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="audit-item-header"
|
||||
onClick={() => toggleExpand(log._id)}
|
||||
disabled={!hasDetail}
|
||||
aria-expanded={hasDetail ? isExpanded : undefined}
|
||||
>
|
||||
<span className={`audit-badge ${ACTION_BADGE[log.action] || 'badge-default'}`}>
|
||||
{ACTION_ICONS[log.action]} {ACTION_LABELS[log.action] || log.action}
|
||||
</span>
|
||||
|
|
@ -244,7 +249,7 @@ function AuditLogs() {
|
|||
{hasDetail && (
|
||||
<span className="expand-toggle">{isExpanded ? '▲' : '▼'}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="audit-item-body">
|
||||
<span className="audit-admin">👤 <strong>{log.adminUsername || '—'}</strong></span>
|
||||
|
|
|
|||
|
|
@ -115,18 +115,18 @@ const DrohnenfuehrerDashboard = ({ drohnenfuehrerUser, onLogout }) => {
|
|||
) : (
|
||||
<form onSubmit={handleSave} className="drohnenfuehrer-edit-form">
|
||||
<div className="form-group">
|
||||
<label>Adresse</label>
|
||||
<input type="text" value={formData.address}
|
||||
<label htmlFor="profil-adresse">Adresse</label>
|
||||
<input id="profil-adresse" type="text" value={formData.address}
|
||||
onChange={e => setFormData({ ...formData, address: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Mobilnummer</label>
|
||||
<input type="tel" value={formData.phone}
|
||||
<label htmlFor="profil-mobil">Mobilnummer</label>
|
||||
<input id="profil-mobil" type="tel" value={formData.phone}
|
||||
onChange={e => setFormData({ ...formData, phone: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Festnetz (optional)</label>
|
||||
<input type="tel" value={formData.landline}
|
||||
<label htmlFor="profil-festnetz">Festnetz (optional)</label>
|
||||
<input id="profil-festnetz" type="tel" value={formData.landline}
|
||||
onChange={e => setFormData({ ...formData, landline: e.target.value })} />
|
||||
</div>
|
||||
<div className="drohnenfuehrer-form-actions">
|
||||
|
|
|
|||
|
|
@ -87,12 +87,12 @@ const DrohnenfuehrerLogin = ({ onLogin }) => {
|
|||
{mode === 'login' ? (
|
||||
<form onSubmit={handleLogin} className="drohnenfuehrer-form">
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="login-email">E-Mail</label>
|
||||
<input id="login-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
<label htmlFor="login-password">Passwort</label>
|
||||
<input id="login-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-drohnenfuehrer-primary" disabled={loading}>
|
||||
{loading ? 'Anmelden...' : 'Anmelden'}
|
||||
|
|
@ -104,20 +104,20 @@ const DrohnenfuehrerLogin = ({ onLogin }) => {
|
|||
Der Admin hat Ihre E-Mail-Adresse hinterlegt. Geben Sie hier Ihre E-Mail, den Einladungs-Token und ein neues Passwort ein.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="setpw-email">E-Mail</label>
|
||||
<input id="setpw-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Einladungs-Token</label>
|
||||
<input type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
<label htmlFor="setpw-token">Einladungs-Token</label>
|
||||
<input id="setpw-token" type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
<label htmlFor="setpw-password">Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input id="setpw-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort wiederholen</label>
|
||||
<input type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
<label htmlFor="setpw-repeat">Passwort wiederholen</label>
|
||||
<input id="setpw-repeat" type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-drohnenfuehrer-primary" disabled={loading}>
|
||||
{loading ? 'Wird gesetzt...' : 'Passwort setzen'}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</div>
|
||||
)}
|
||||
<div className="filter-group">
|
||||
<label>Typ:</label>
|
||||
<select
|
||||
<label htmlFor="filter-typ">Typ:</label>
|
||||
<select id="filter-typ"
|
||||
value={filters.type || ''}
|
||||
onChange={(e) => onFilterChange('type', e.target.value || null)}
|
||||
className="filter-select"
|
||||
|
|
@ -35,8 +35,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Sortierung:</label>
|
||||
<select
|
||||
<label htmlFor="filter-sortierung">Sortierung:</label>
|
||||
<select id="filter-sortierung"
|
||||
value={filters.sortBy || 'name'}
|
||||
onChange={(e) => onFilterChange('sortBy', e.target.value)}
|
||||
className="filter-select"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
in beiden Varianten nachgerechnet.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
/* Sagt dem Browser, dass die Seite beide Ansichten kann. Native Bedienelemente
|
||||
— Checkboxen, Auswahllisten, Bildlaufleisten — passen sich dadurch an,
|
||||
statt in der Nachtansicht hell aufzublitzen. */
|
||||
color-scheme: light dark;
|
||||
|
||||
--font-display: Georgia, 'Times New Roman', serif;
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
|
@ -159,9 +164,6 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
/* Damit auch vom Browser gestellte Bedienelemente (Bildlaufleisten,
|
||||
Datumsauswahl, Autofill) der gewählten Ansicht folgen. */
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Formularfelder brauchen ausdrücklich Farben: ohne sie nimmt der Browser
|
||||
|
|
@ -179,6 +181,13 @@ textarea::placeholder {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Checkbox, Radio und Schieberegler in der Markenfarbe statt im Browser-Blau. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
input[type="range"] {
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
.admin-tabs {
|
||||
display: inline-flex;
|
||||
/* Vier Reiter passen auf schmalen Displays nicht in eine Zeile — ohne
|
||||
Umbruch lief die Seite bei 360 px horizontal ueber. */
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -14,6 +17,7 @@
|
|||
}
|
||||
|
||||
.tab-button {
|
||||
min-height: var(--touch-target);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
|
@ -122,7 +126,7 @@
|
|||
.settings-section-count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 999px;
|
||||
padding: 0.1rem 0.5rem;
|
||||
}
|
||||
|
|
@ -164,7 +168,7 @@
|
|||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -346,3 +350,16 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1 1 45%;
|
||||
padding: 0.5rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,8 +371,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
</div>
|
||||
<div className="settings-section-body">
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input
|
||||
<label className="settings-label" htmlFor="settings-appname">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input id="settings-appname"
|
||||
type="text"
|
||||
className="settings-input"
|
||||
value={config.appName || ''}
|
||||
|
|
@ -439,8 +439,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
{ key: 'ansprechpartner', label: 'Ansprechpartner und Koordination' }
|
||||
].map(({ key, label }) => (
|
||||
<div key={key} className="settings-field">
|
||||
<label className="settings-label">{label}</label>
|
||||
<textarea
|
||||
<label className="settings-label" htmlFor={`settings-section-${key}`}>{label}</label>
|
||||
<textarea id={`settings-section-${key}`}
|
||||
value={getSectionContent(key)}
|
||||
onChange={(e) => updateSection(key, e.target.value)}
|
||||
className="settings-textarea settings-textarea-lg"
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
.badge-login-failed { background: #ffcdd2; color: #b71c1c; font-weight: 700; }
|
||||
.badge-import { background: #e0f7fa; color: #00695c; }
|
||||
.badge-export { background: #e8f5e9; color: #1b5e20; }
|
||||
.badge-bulk-update { background: var(--color-surface-alt); color: #283593; }
|
||||
.badge-bulk-update { background: #e8eaf6; color: #283593; }
|
||||
.badge-bulk-delete { background: #fbe9e7; color: #bf360c; }
|
||||
.badge-password { background: #fff8e1; color: #9c4e00; }
|
||||
.badge-default { background: #f5f5f5; color: #616161; }
|
||||
|
|
@ -280,14 +280,25 @@
|
|||
.audit-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.audit-item-failed { border-left: 4px solid var(--color-danger); }
|
||||
|
||||
/* Als <button> ausgezeichnet, damit das Auf- und Zuklappen auch mit der
|
||||
Tastatur erreichbar ist. Die Button-Grundstile werden hier zurueckgesetzt. */
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: var(--color-surface-alt);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.audit-item-header:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.audit-time { margin-left: auto; color: var(--color-text-muted); font-size: 12px; white-space: nowrap; }
|
||||
.expand-toggle { color: var(--color-text-muted); font-size: 11px; cursor: pointer; padding: 0 4px; }
|
||||
|
|
@ -502,13 +513,6 @@
|
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.audit-badge {
|
||||
padding: 6px 12px;
|
||||
|
|
|
|||
|
|
@ -228,8 +228,13 @@ function AuditLogs() {
|
|||
|
||||
return (
|
||||
<div key={log._id} className={`audit-item ${!log.success ? 'audit-item-failed' : ''}`}>
|
||||
<div className="audit-item-header" onClick={() => hasDetail && toggleExpand(log._id)}
|
||||
style={{ cursor: hasDetail ? 'pointer' : 'default' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="audit-item-header"
|
||||
onClick={() => toggleExpand(log._id)}
|
||||
disabled={!hasDetail}
|
||||
aria-expanded={hasDetail ? isExpanded : undefined}
|
||||
>
|
||||
<span className={`audit-badge ${ACTION_BADGE[log.action] || 'badge-default'}`}>
|
||||
{ACTION_ICONS[log.action]} {ACTION_LABELS[log.action] || log.action}
|
||||
</span>
|
||||
|
|
@ -244,7 +249,7 @@ function AuditLogs() {
|
|||
{hasDetail && (
|
||||
<span className="expand-toggle">{isExpanded ? '▲' : '▼'}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="audit-item-body">
|
||||
<span className="audit-admin">👤 <strong>{log.adminUsername || '—'}</strong></span>
|
||||
|
|
|
|||
|
|
@ -115,18 +115,18 @@ const HandlerDashboard = ({ handlerUser, onLogout }) => {
|
|||
) : (
|
||||
<form onSubmit={handleSave} className="handler-edit-form">
|
||||
<div className="form-group">
|
||||
<label>Adresse</label>
|
||||
<input type="text" value={formData.address}
|
||||
<label htmlFor="profil-adresse">Adresse</label>
|
||||
<input id="profil-adresse" type="text" value={formData.address}
|
||||
onChange={e => setFormData({ ...formData, address: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Mobilnummer</label>
|
||||
<input type="tel" value={formData.phone}
|
||||
<label htmlFor="profil-mobil">Mobilnummer</label>
|
||||
<input id="profil-mobil" type="tel" value={formData.phone}
|
||||
onChange={e => setFormData({ ...formData, phone: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Festnetz (optional)</label>
|
||||
<input type="tel" value={formData.landline}
|
||||
<label htmlFor="profil-festnetz">Festnetz (optional)</label>
|
||||
<input id="profil-festnetz" type="tel" value={formData.landline}
|
||||
onChange={e => setFormData({ ...formData, landline: e.target.value })} />
|
||||
</div>
|
||||
<div className="handler-form-actions">
|
||||
|
|
|
|||
|
|
@ -87,12 +87,12 @@ const HandlerLogin = ({ onLogin }) => {
|
|||
{mode === 'login' ? (
|
||||
<form onSubmit={handleLogin} className="handler-form">
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="login-email">E-Mail</label>
|
||||
<input id="login-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
<label htmlFor="login-password">Passwort</label>
|
||||
<input id="login-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-handler-primary" disabled={loading}>
|
||||
{loading ? 'Anmelden...' : 'Anmelden'}
|
||||
|
|
@ -104,20 +104,20 @@ const HandlerLogin = ({ onLogin }) => {
|
|||
Der Admin hat Ihre E-Mail-Adresse hinterlegt. Geben Sie hier Ihre E-Mail, den Einladungs-Token und ein neues Passwort ein.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="setpw-email">E-Mail</label>
|
||||
<input id="setpw-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Einladungs-Token</label>
|
||||
<input type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
<label htmlFor="setpw-token">Einladungs-Token</label>
|
||||
<input id="setpw-token" type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
<label htmlFor="setpw-password">Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input id="setpw-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort wiederholen</label>
|
||||
<input type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
<label htmlFor="setpw-repeat">Passwort wiederholen</label>
|
||||
<input id="setpw-repeat" type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-handler-primary" disabled={loading}>
|
||||
{loading ? 'Wird gesetzt...' : 'Passwort setzen'}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</div>
|
||||
)}
|
||||
<div className="filter-group">
|
||||
<label>Typ:</label>
|
||||
<select
|
||||
<label htmlFor="filter-typ">Typ:</label>
|
||||
<select id="filter-typ"
|
||||
value={filters.type || ''}
|
||||
onChange={(e) => onFilterChange('type', e.target.value || null)}
|
||||
className="filter-select"
|
||||
|
|
@ -35,8 +35,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Sortierung:</label>
|
||||
<select
|
||||
<label htmlFor="filter-sortierung">Sortierung:</label>
|
||||
<select id="filter-sortierung"
|
||||
value={filters.sortBy || 'name'}
|
||||
onChange={(e) => onFilterChange('sortBy', e.target.value)}
|
||||
className="filter-select"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
in beiden Varianten nachgerechnet.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
/* Sagt dem Browser, dass die Seite beide Ansichten kann. Native Bedienelemente
|
||||
— Checkboxen, Auswahllisten, Bildlaufleisten — passen sich dadurch an,
|
||||
statt in der Nachtansicht hell aufzublitzen. */
|
||||
color-scheme: light dark;
|
||||
|
||||
--font-display: Georgia, 'Times New Roman', serif;
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
|
|
@ -159,9 +164,6 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
/* Damit auch vom Browser gestellte Bedienelemente (Bildlaufleisten,
|
||||
Datumsauswahl, Autofill) der gewählten Ansicht folgen. */
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Formularfelder brauchen ausdrücklich Farben: ohne sie nimmt der Browser
|
||||
|
|
@ -179,6 +181,13 @@ textarea::placeholder {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Checkbox, Radio und Schieberegler in der Markenfarbe statt im Browser-Blau. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
input[type="range"] {
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
.admin-tabs {
|
||||
display: inline-flex;
|
||||
/* Vier Reiter passen auf schmalen Displays nicht in eine Zeile — ohne
|
||||
Umbruch lief die Seite bei 360 px horizontal ueber. */
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -14,6 +17,7 @@
|
|||
}
|
||||
|
||||
.tab-button {
|
||||
min-height: var(--touch-target);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
|
@ -122,7 +126,7 @@
|
|||
.settings-section-count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 999px;
|
||||
padding: 0.1rem 0.5rem;
|
||||
}
|
||||
|
|
@ -164,7 +168,7 @@
|
|||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-border);
|
||||
background: var(--color-surface-alt);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -346,3 +350,16 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1 1 45%;
|
||||
padding: 0.5rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,8 +371,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
</div>
|
||||
<div className="settings-section-body">
|
||||
<div className="settings-field">
|
||||
<label className="settings-label">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input
|
||||
<label className="settings-label" htmlFor="settings-appname">Name der App (wird in der Kopfzeile angezeigt)</label>
|
||||
<input id="settings-appname"
|
||||
type="text"
|
||||
className="settings-input"
|
||||
value={config.appName || ''}
|
||||
|
|
@ -439,8 +439,8 @@ const AdminPanel = ({ users, loading, error, onRefetch }) => {
|
|||
{ key: 'ansprechpartner', label: 'Ansprechpartner und Koordination' }
|
||||
].map(({ key, label }) => (
|
||||
<div key={key} className="settings-field">
|
||||
<label className="settings-label">{label}</label>
|
||||
<textarea
|
||||
<label className="settings-label" htmlFor={`settings-section-${key}`}>{label}</label>
|
||||
<textarea id={`settings-section-${key}`}
|
||||
value={getSectionContent(key)}
|
||||
onChange={(e) => updateSection(key, e.target.value)}
|
||||
className="settings-textarea settings-textarea-lg"
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
.badge-login-failed { background: #ffcdd2; color: #b71c1c; font-weight: 700; }
|
||||
.badge-import { background: #e0f7fa; color: #00695c; }
|
||||
.badge-export { background: #e8f5e9; color: #1b5e20; }
|
||||
.badge-bulk-update { background: var(--color-surface-alt); color: #283593; }
|
||||
.badge-bulk-update { background: #e8eaf6; color: #283593; }
|
||||
.badge-bulk-delete { background: #fbe9e7; color: #bf360c; }
|
||||
.badge-password { background: #fff8e1; color: #9c4e00; }
|
||||
.badge-default { background: #f5f5f5; color: #616161; }
|
||||
|
|
@ -280,14 +280,25 @@
|
|||
.audit-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.audit-item-failed { border-left: 4px solid var(--color-danger); }
|
||||
|
||||
/* Als <button> ausgezeichnet, damit das Auf- und Zuklappen auch mit der
|
||||
Tastatur erreichbar ist. Die Button-Grundstile werden hier zurueckgesetzt. */
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: var(--color-surface-alt);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.audit-item-header:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.audit-time { margin-left: auto; color: var(--color-text-muted); font-size: 12px; white-space: nowrap; }
|
||||
.expand-toggle { color: var(--color-text-muted); font-size: 11px; cursor: pointer; padding: 0 4px; }
|
||||
|
|
@ -502,13 +513,6 @@
|
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.audit-item-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.audit-badge {
|
||||
padding: 6px 12px;
|
||||
|
|
|
|||
|
|
@ -228,8 +228,13 @@ function AuditLogs() {
|
|||
|
||||
return (
|
||||
<div key={log._id} className={`audit-item ${!log.success ? 'audit-item-failed' : ''}`}>
|
||||
<div className="audit-item-header" onClick={() => hasDetail && toggleExpand(log._id)}
|
||||
style={{ cursor: hasDetail ? 'pointer' : 'default' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="audit-item-header"
|
||||
onClick={() => toggleExpand(log._id)}
|
||||
disabled={!hasDetail}
|
||||
aria-expanded={hasDetail ? isExpanded : undefined}
|
||||
>
|
||||
<span className={`audit-badge ${ACTION_BADGE[log.action] || 'badge-default'}`}>
|
||||
{ACTION_ICONS[log.action]} {ACTION_LABELS[log.action] || log.action}
|
||||
</span>
|
||||
|
|
@ -244,7 +249,7 @@ function AuditLogs() {
|
|||
{hasDetail && (
|
||||
<span className="expand-toggle">{isExpanded ? '▲' : '▼'}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="audit-item-body">
|
||||
<span className="audit-admin">👤 <strong>{log.adminUsername || '—'}</strong></span>
|
||||
|
|
|
|||
|
|
@ -115,18 +115,18 @@ const StoeberhundefuehrerDashboard = ({ stoeberhundefuehrerUser, onLogout }) =>
|
|||
) : (
|
||||
<form onSubmit={handleSave} className="stoeberhundefuehrer-edit-form">
|
||||
<div className="form-group">
|
||||
<label>Adresse</label>
|
||||
<input type="text" value={formData.address}
|
||||
<label htmlFor="profil-adresse">Adresse</label>
|
||||
<input id="profil-adresse" type="text" value={formData.address}
|
||||
onChange={e => setFormData({ ...formData, address: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Mobilnummer</label>
|
||||
<input type="tel" value={formData.phone}
|
||||
<label htmlFor="profil-mobil">Mobilnummer</label>
|
||||
<input id="profil-mobil" type="tel" value={formData.phone}
|
||||
onChange={e => setFormData({ ...formData, phone: e.target.value })} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Festnetz (optional)</label>
|
||||
<input type="tel" value={formData.landline}
|
||||
<label htmlFor="profil-festnetz">Festnetz (optional)</label>
|
||||
<input id="profil-festnetz" type="tel" value={formData.landline}
|
||||
onChange={e => setFormData({ ...formData, landline: e.target.value })} />
|
||||
</div>
|
||||
<div className="stoeberhundefuehrer-form-actions">
|
||||
|
|
|
|||
|
|
@ -87,12 +87,12 @@ const StoeberhundefuehrerLogin = ({ onLogin }) => {
|
|||
{mode === 'login' ? (
|
||||
<form onSubmit={handleLogin} className="stoeberhundefuehrer-form">
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="login-email">E-Mail</label>
|
||||
<input id="login-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
<label htmlFor="login-password">Passwort</label>
|
||||
<input id="login-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-stoeberhundefuehrer-primary" disabled={loading}>
|
||||
{loading ? 'Anmelden...' : 'Anmelden'}
|
||||
|
|
@ -104,20 +104,20 @@ const StoeberhundefuehrerLogin = ({ onLogin }) => {
|
|||
Der Admin hat Ihre E-Mail-Adresse hinterlegt. Geben Sie hier Ihre E-Mail, den Einladungs-Token und ein neues Passwort ein.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
<label htmlFor="setpw-email">E-Mail</label>
|
||||
<input id="setpw-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Einladungs-Token</label>
|
||||
<input type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
<label htmlFor="setpw-token">Einladungs-Token</label>
|
||||
<input id="setpw-token" type="text" value={inviteToken} onChange={e => setInviteToken(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
<label htmlFor="setpw-password">Neues Passwort (min. 8 Zeichen)</label>
|
||||
<input id="setpw-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required minLength={8} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Passwort wiederholen</label>
|
||||
<input type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
<label htmlFor="setpw-repeat">Passwort wiederholen</label>
|
||||
<input id="setpw-repeat" type="password" value={newPassword2} onChange={e => setNewPassword2(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn-stoeberhundefuehrer-primary" disabled={loading}>
|
||||
{loading ? 'Wird gesetzt...' : 'Passwort setzen'}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</div>
|
||||
)}
|
||||
<div className="filter-group">
|
||||
<label>Typ:</label>
|
||||
<select
|
||||
<label htmlFor="filter-typ">Typ:</label>
|
||||
<select id="filter-typ"
|
||||
value={filters.type || ''}
|
||||
onChange={(e) => onFilterChange('type', e.target.value || null)}
|
||||
className="filter-select"
|
||||
|
|
@ -35,8 +35,8 @@ const FilterPanel = ({ filters, onFilterChange, showAvailableFilter = true }) =>
|
|||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Sortierung:</label>
|
||||
<select
|
||||
<label htmlFor="filter-sortierung">Sortierung:</label>
|
||||
<select id="filter-sortierung"
|
||||
value={filters.sortBy || 'name'}
|
||||
onChange={(e) => onFilterChange('sortBy', e.target.value)}
|
||||
className="filter-select"
|
||||
|
|
|
|||
Loading…
Reference in New Issue