Design-System: Token-Ebene für Spacing, Fokus und Tap-Targets

Vorbereitung für die Konsolidierung. tokens.css war für Farben und Typo schon
die Single Source of Truth, für Abstände aber nur mit --space-section und
--container-pad besetzt — dem standen ~450 Spacing-Deklarationen mit 39
verschiedenen rem-Werten gegenüber.

Neu: 14-stufige Spacing-Skala auf 4px-Raster, aus dem Ist-Bestand geclustert
statt frei erfunden. Die neun häufigsten Werte der Codebase (1, 1.5, 1.25, 2.5,
0.75, 0.5, 1.75, 2, 3rem — zusammen ~60% aller Deklarationen) liegen bereits
exakt auf Stufen, die Migration ist dort verlustfrei. Nach oben wird das Raster
gröber (3 → 4 → 5 → 7rem), weil große Abstände das vertragen. --space-4xs (2px)
ist ausdrücklich nur für optische Korrekturen an Icon-/Baseline-Nudges.

Dazu --tap-min (44px, WCAG 2.5.5) und --focus-ring/--focus-offset. Der Fokus
wird auf Weiß festgeschrieben: 15,9:1 auf --clr-bg gegenüber 3,2:1 für Rot,
das WCAG 1.4.11 nur knapp schafft und als einziger Indikator zu schwach ist.

--container-pad und --space-section verweisen jetzt auf die Skala statt eigene
Werte zu führen. Entfernt: --clr-bg-light und --clr-surface-raised, beide
projektweit 0× verwendet.

Consumer folgen in den nächsten Commits — dieser Schritt ändert die Darstellung
noch nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
This commit is contained in:
2026-07-27 10:07:31 +02:00
parent 96a698ebdc
commit 8304095dc3
6 changed files with 4736 additions and 140 deletions

View File

@@ -16,8 +16,10 @@ declare(strict_types=1);
* label_html NUR für vertrauenswürdiges Markup aus Komponenten-Code
* (z. B. Datenschutz-Link im Checkbox-Label) — nie Nutzereingaben!
*
* Jedes Feld bekommt ein leeres Fehler-Element (#<id>-error), das form.js bei
* ungültiger Eingabe mit der Browser-Validierungsmeldung füllt (aria-describedby).
* Jedes Feld bekommt ein Fehler-Element (#<id>-error, aria-describedby): serverseitig
* gefüllt beim 422-Re-Render (form_field_error()), sonst von form.js mit der
* Browser-Validierungsmeldung. Eingaben aus einem abgewiesenen POST schreibt
* form_old() zurück — deshalb gehen Werte auch ohne JS nie verloren.
*/
$type = $field['type'] ?? 'text';
$name = $field['name'];
@@ -26,6 +28,8 @@ $required = !empty($field['required']);
$errorId = $id . '-error';
$hintId = $id . '-hint';
$describedBy = (!empty($field['hint']) ? $hintId . ' ' : '') . $errorId;
$old = form_old($name);
$fieldError = form_field_error($id);
// Sichtbarer roter Stern (aria-hidden) + pro Feld erklärendes Pflicht-Wort für
// Screenreader — ersetzt die frühere globale „Pflichtfelder mit *"-Hinweiszeile.
@@ -39,13 +43,14 @@ $commonAttrs = 'name="' . e($name) . '" id="' . e($id) . '"'
. (!empty($field['autocomplete']) ? ' autocomplete="' . e($field['autocomplete']) . '"' : '')
. (!empty($field['maxlength']) ? ' maxlength="' . e($field['maxlength']) . '"' : '')
. (isset($field['min']) ? ' min="' . e((string) $field['min']) . '"' : '')
. (isset($field['max']) ? ' max="' . e((string) $field['max']) . '"' : '');
. (isset($field['max']) ? ' max="' . e((string) $field['max']) . '"' : '')
. ($fieldError !== '' ? ' aria-invalid="true"' : '');
?>
<?php if ($type === 'checkbox'): ?>
<div class="form__field form__field--checkbox">
<input type="checkbox" <?= $commonAttrs ?> value="1">
<input type="checkbox" <?= $commonAttrs ?> value="1"<?= $old === '1' ? ' checked' : '' ?>>
<label for="<?= e($id) ?>"><?= $field['label_html'] ?? e($field['label']) ?><?= $requiredMark ?></label>
<p class="form__error-msg" id="<?= e($errorId) ?>" hidden></p>
<p class="form__error-msg" id="<?= e($errorId) ?>"<?= $fieldError === '' ? ' hidden' : '' ?>><?= e($fieldError) ?></p>
</div>
<?php else: ?>
<div class="form__field">
@@ -54,17 +59,17 @@ $commonAttrs = 'name="' . e($name) . '" id="' . e($id) . '"'
<select <?= $commonAttrs ?>>
<option value=""><?= e($field['placeholder'] ?? 'Bitte wählen') ?></option>
<?php foreach ($field['options'] as $option): ?>
<option value="<?= e($option) ?>"><?= e($option) ?></option>
<option value="<?= e($option) ?>"<?= $old !== '' && $old === (string) $option ? ' selected' : '' ?>><?= e($option) ?></option>
<?php endforeach; ?>
</select>
<?php elseif ($type === 'textarea'): ?>
<textarea <?= $commonAttrs ?> rows="<?= e($field['rows'] ?? 6) ?>" placeholder=" "></textarea>
<textarea <?= $commonAttrs ?> rows="<?= e($field['rows'] ?? 6) ?>" placeholder=" "><?= e($old) ?></textarea>
<?php else: ?>
<input type="<?= e($type) ?>" <?= $commonAttrs ?> placeholder=" ">
<input type="<?= e($type) ?>" <?= $commonAttrs ?> value="<?= e($old) ?>" placeholder=" ">
<?php endif; ?>
<?php if (!empty($field['hint'])): ?>
<p class="form__hint" id="<?= e($hintId) ?>"><?= e($field['hint']) ?></p>
<?php endif; ?>
<p class="form__error-msg" id="<?= e($errorId) ?>" hidden></p>
<p class="form__error-msg" id="<?= e($errorId) ?>"<?= $fieldError === '' ? ' hidden' : '' ?>><?= e($fieldError) ?></p>
</div>
<?php endif; ?>