Formular-Regeln in CLAUDE.md; A11y-Fixes: Feldränder 3:1-Kontrast (neuer Token), Checkbox 24px + größeres Label, aria-live-Region nie display:none
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
22
CLAUDE.md
22
CLAUDE.md
@@ -38,6 +38,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
|
|||||||
| Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` |
|
| Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` |
|
||||||
| Partner/Sponsoren | `data/partners.json` | — |
|
| Partner/Sponsoren | `data/partners.json` | — |
|
||||||
| Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** |
|
| Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** |
|
||||||
|
| Icons | `public/assets/icons/` (Bootstrap Icons, lokal) via `icon()`-Helper | Inline-SVG-Pfade von Hand ins Markup, Emoji/Unicode-Glyphen, gezeichnete CSS-Icons, Icon-Font/CDN |
|
||||||
|
|
||||||
Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pages/*.php`) im `$meta`-Array.
|
Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pages/*.php`) im `$meta`-Array.
|
||||||
|
|
||||||
@@ -50,7 +51,8 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag
|
|||||||
prüfen ob eine Komponente existiert und erweitert werden kann. Wiederkehrende Inhalte auf neuen
|
prüfen ob eine Komponente existiert und erweitert werden kann. Wiederkehrende Inhalte auf neuen
|
||||||
Seiten → als Komponente extrahieren.
|
Seiten → als Komponente extrahieren.
|
||||||
- **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()`, `asset()`
|
- **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()`, `asset()`
|
||||||
(Versionierung via filemtime), `json_load('name')` (liest `data/name.json`, static cache), `component()`.
|
(Versionierung via filemtime), `json_load('name')` (liest `data/name.json`, static cache), `component()`,
|
||||||
|
`icon('name', 'klasse', 'label?')` (Inline-SVG aus `public/assets/icons/`, siehe Abschnitt Icons).
|
||||||
- **CSS:** 6 Dateien als `<link>` in fester Reihenfolge: tokens → reset → base → layout → components →
|
- **CSS:** 6 Dateien als `<link>` in fester Reihenfolge: tokens → reset → base → layout → components →
|
||||||
utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit
|
utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit
|
||||||
Banner-Kommentaren (`/* === hero === */`).
|
Banner-Kommentaren (`/* === hero === */`).
|
||||||
@@ -61,6 +63,17 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag
|
|||||||
|
|
||||||
## Formulare
|
## Formulare
|
||||||
|
|
||||||
|
- **Felder ausschließlich über `component('form-field', ['field' => …])`** (`app/components/form-field.php`) —
|
||||||
|
nie Feld-Markup von Hand. Die Komponente liefert Label, Pflichtfeld-Stern, Hilfetext und das
|
||||||
|
Fehler-Element `#<id>-error` (per `aria-describedby` verknüpft) in einheitlichem Markup.
|
||||||
|
- **Jedes neue Formular braucht:** (1) Status-Region `role="status" aria-live="polite" tabindex="-1"
|
||||||
|
data-form-status` als erstes Form-Kind, (2) Hinweiszeile „Pflichtfelder sind mit * markiert“,
|
||||||
|
(3) Honeypot-Feld `website` (visually-hidden) + Hidden-Field `ft` mit `form_token()`,
|
||||||
|
(4) serverseitige Action unter `app/actions/` mit Whitelist-Validierung + PRG-Redirect.
|
||||||
|
- `form.js` übernimmt automatisch Inline-Feldfehler (Browser-Validierungsmeldung, `aria-invalid`,
|
||||||
|
Fokus aufs erste ungültige Feld) — Konvention dafür ist die `#<id>-error`-ID aus der Komponente.
|
||||||
|
- A11y-Vorgaben: Feldränder mit `--clr-glass-border-strong` (≥3:1 Kontrast, WCAG 1.4.11), Checkbox
|
||||||
|
min. 24px (WCAG 2.5.8), Status-Region nie per `display:none` verstecken (fliegt aus dem Accessibility-Tree).
|
||||||
- Versand via PHPMailer über Brevo SMTP (`smtp-relay.brevo.com:587`, STARTTLS), Config aus `config/config.php`.
|
- Versand via PHPMailer über Brevo SMTP (`smtp-relay.brevo.com:587`, STARTTLS), Config aus `config/config.php`.
|
||||||
- Spam-Schutz ohne externe Dienste: Honeypot-Feld + HMAC-signierter Timestamp (Time-Trap, `app_secret`)
|
- Spam-Schutz ohne externe Dienste: Honeypot-Feld + HMAC-signierter Timestamp (Time-Trap, `app_secret`)
|
||||||
+ serverseitige Validierung mit Whitelists. POST-Redirect-GET; `form.js` macht optional fetch.
|
+ serverseitige Validierung mit Whitelists. POST-Redirect-GET; `form.js` macht optional fetch.
|
||||||
@@ -74,6 +87,13 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag
|
|||||||
- Rot nie für Fließtext auf dunklem Grund (Kontrast!) — nur als Button-/Akzent-Fläche mit weißem Text.
|
- Rot nie für Fließtext auf dunklem Grund (Kontrast!) — nur als Button-/Akzent-Fläche mit weißem Text.
|
||||||
- Alle Werte als CSS Custom Properties in `tokens.css`; Breakpoints: 1249 (Burger) / 1024 / 992 / 768 / 600 / 500.
|
- Alle Werte als CSS Custom Properties in `tokens.css`; Breakpoints: 1249 (Burger) / 1024 / 992 / 768 / 600 / 500.
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
- **Bootstrap Icons** (MIT), lokal & self-hosted unter `public/assets/icons/<name>.svg` — die erlaubte Icon-Quelle. Kein Icon-Font, kein CDN, keine gezeichneten CSS-/Unicode-Icons.
|
||||||
|
- Ausgabe **nur** über den Helper: `icon('pause-fill')` (dekorativ, `aria-hidden`), `icon('pause-fill', 'meine-klasse')` (mit Klasse), `icon('envelope', '', 'E-Mail')` (semantisch, `role="img"` + `aria-label`). Inline-SVG, currentColor erbt die Schriftfarbe.
|
||||||
|
- Größe = `font-size` des Elements (Icon ist `1em`), Farbe = `currentColor`. Basisklasse `.icon` in `utilities.css`.
|
||||||
|
- Nur tatsächlich genutzte Icons werden abgelegt (kein Komplett-Set). Neues Icon: `php bin/icons-add.php <name>` (Name = Dateiname auf icons.getbootstrap.com ohne `.svg`).
|
||||||
|
|
||||||
## Checkliste für jede neue Seite/Komponente
|
## Checkliste für jede neue Seite/Komponente
|
||||||
|
|
||||||
- [ ] Semantisches HTML (Landmarks, Headings-Hierarchie ohne Sprünge)
|
- [ ] Semantisches HTML (Landmarks, Headings-Hierarchie ohne Sprünge)
|
||||||
|
|||||||
@@ -331,7 +331,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: var(--clr-glass-bg);
|
background: var(--clr-glass-bg);
|
||||||
border: 1px solid var(--clr-glass-border);
|
border: 1px solid var(--clr-glass-border-strong);
|
||||||
border-radius: var(--radius-btn);
|
border-radius: var(--radius-btn);
|
||||||
color: var(--clr-text);
|
color: var(--clr-text);
|
||||||
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
|
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
.form__field input:hover,
|
.form__field input:hover,
|
||||||
.form__field select:hover,
|
.form__field select:hover,
|
||||||
.form__field textarea:hover {
|
.form__field textarea:hover {
|
||||||
border-color: rgb(255 255 255 / 0.4);
|
border-color: rgb(255 255 255 / 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Eigener, deutlich sichtbarer Fokus-Ring statt des globalen Outlines */
|
/* Eigener, deutlich sichtbarer Fokus-Ring statt des globalen Outlines */
|
||||||
@@ -416,19 +416,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form__field--checkbox input {
|
.form__field--checkbox input {
|
||||||
width: 1.25rem;
|
/* 24px — Mindest-Zielgröße (WCAG 2.5.8) */
|
||||||
height: 1.25rem;
|
width: 1.5rem;
|
||||||
margin-top: 0.2rem;
|
height: 1.5rem;
|
||||||
|
margin-top: 0.1rem;
|
||||||
accent-color: var(--clr-accent);
|
accent-color: var(--clr-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__field--checkbox label {
|
.form__field--checkbox label {
|
||||||
font-size: var(--fs-400);
|
font-size: var(--fs-500);
|
||||||
color: var(--clr-text-muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Leer: unsichtbar, aber im Accessibility-Tree — display:none würde die
|
||||||
|
aria-live-Region abmelden und Screenreader verpassen die erste Meldung. */
|
||||||
.form__status:empty {
|
.form__status:empty {
|
||||||
display: none;
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__success {
|
.form__success {
|
||||||
@@ -601,25 +607,21 @@
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pause-Symbol (zwei Balken) im Abspiel-Zustand … */
|
.ad-banner__toggle-icon {
|
||||||
.ad-banner__toggle::before {
|
font-size: 1.15rem;
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 4px;
|
|
||||||
height: 13px;
|
|
||||||
border-left: 4px solid currentColor;
|
|
||||||
border-right: 4px solid currentColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* … Play-Dreieck, wenn pausiert. */
|
/* Pause-Icon im Abspiel-Zustand, Play-Icon wenn pausiert. */
|
||||||
.ad-banner__toggle[aria-pressed="true"]::before {
|
.ad-banner__toggle .ad-banner__toggle-icon--play {
|
||||||
width: 0;
|
display: none;
|
||||||
height: 0;
|
}
|
||||||
margin-left: 3px;
|
|
||||||
border: 0;
|
.ad-banner__toggle[aria-pressed="true"] .ad-banner__toggle-icon--pause {
|
||||||
border-left: 13px solid currentColor;
|
display: none;
|
||||||
border-top: 8px solid transparent;
|
}
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
|
.ad-banner__toggle[aria-pressed="true"] .ad-banner__toggle-icon--play {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Indikatorleiste — die aktive Leiste füllt sich über --banner-interval. */
|
/* Indikatorleiste — die aktive Leiste füllt sich über --banner-interval. */
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
--clr-text-faint: #8c8c8c; /* 4.5:1 auf --clr-bg (AA) */
|
--clr-text-faint: #8c8c8c; /* 4.5:1 auf --clr-bg (AA) */
|
||||||
--clr-glass-bg: rgb(255 255 255 / 0.11);
|
--clr-glass-bg: rgb(255 255 255 / 0.11);
|
||||||
--clr-glass-border: rgb(255 255 255 / 0.22);
|
--clr-glass-border: rgb(255 255 255 / 0.22);
|
||||||
|
--clr-glass-border-strong: rgb(255 255 255 / 0.4); /* Formularfelder: ≥3:1 Kontrast (WCAG 1.4.11) */
|
||||||
--clr-error: #ff8a8a; /* Formularfehler, lesbar auf dunkel */
|
--clr-error: #ff8a8a; /* Formularfehler, lesbar auf dunkel */
|
||||||
--clr-success: #8ce99a;
|
--clr-success: #8ce99a;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user