Direktbearbeitung, Kontrastwarnung und Gestaltungsvorlagen
Inline-Textbearbeitung ohne Skript in der Vorschau: der Iframe führt weiterhin kein eigenes JavaScript aus, der Editor greift in sein Dokument und setzt das Element auf contenteditable. Während der Bearbeitung wird die Vorschau nicht neu geladen, sonst wäre der Cursor weg; Eingaben werden zu einem Undo-Schritt zusammengefasst. Neben dem Doppelklick startet eine Schaltfläche dasselbe ohne Maus. Kontrast wird nach WCAG 2.1 berechnet und als Warnung in drei Stufen gemeldet, nicht als Verbot - der Plan verlangt Prüfung, nicht Bevormundung. Gestaltungsvorlagen liegen im Theme und gelten seitenübergreifend. Sie werden mit demselben Compiler geprüft wie ein Blockstil, also kann über diesen Weg kein Wert hereinkommen, für den ein Block abgewiesen würde. Dazu Kopieren und Einfügen zwischen Blöcken und Bildauswahl für Hintergrundbilder über die Mediathek. Geprüft: 155 PHP-Tests / 579 Assertions unter PHP 8.2.12 und 8.4.22, 15 JavaScript-Modelltests, Lint/Format/Container. Erstmals wurde der neue Editor im laufenden Betrieb bedient: Direktbearbeitung und Kontrastwarnung mit echten Werten ausgelöst, Teständerungen verworfen. Weiterhin keine axe- oder Lighthouse-Messung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abf0bd2c6b
commit
270cbda9ac
11 files changed
+388
-10
No files matched your search
+43
-2
@@ -14,6 +14,7 @@ final class Theme
|
||||
{
|
||||
public const MAX_COLORS = 40;
|
||||
public const MAX_FONTS = 8;
|
||||
public const MAX_PRESETS = 40;
|
||||
/** Weights Google serves for most families; also the range a designer realistically needs. */
|
||||
public const WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
|
||||
@@ -57,10 +58,15 @@ final class Theme
|
||||
$raw = $this->db->fetchOne('SELECT theme FROM website WHERE id = ?', ['default']);
|
||||
$stored = is_string($raw) ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : [];
|
||||
if (!is_array($stored) || !isset($stored['colors'], $stored['fonts'], $stored['base'])) {
|
||||
return self::DEFAULTS;
|
||||
return [...self::DEFAULTS, 'presets' => []];
|
||||
}
|
||||
|
||||
return ['colors' => $stored['colors'], 'fonts' => $stored['fonts'], 'base' => [...self::DEFAULTS['base'], ...$stored['base']]];
|
||||
return [
|
||||
'colors' => $stored['colors'],
|
||||
'fonts' => $stored['fonts'],
|
||||
'base' => [...self::DEFAULTS['base'], ...$stored['base']],
|
||||
'presets' => is_array($stored['presets'] ?? null) ? $stored['presets'] : [],
|
||||
];
|
||||
}
|
||||
|
||||
public function save(array $colors, array $fonts, array $base): void
|
||||
@@ -69,6 +75,8 @@ final class Theme
|
||||
'colors' => $this->colors($colors),
|
||||
'fonts' => $this->fonts($fonts),
|
||||
'base' => $this->base($base),
|
||||
// Saving the palette must not drop the presets an editor has collected.
|
||||
'presets' => $this->load()['presets'],
|
||||
];
|
||||
$keys = array_column($theme['colors'], 'key');
|
||||
foreach (['color', 'backgroundColor', 'linkColor'] as $field) {
|
||||
@@ -198,6 +206,39 @@ final class Theme
|
||||
return $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a reusable style under a name, or removes it when the style is null. The style is
|
||||
* validated with the same compiler the documents use, so a preset can never carry a value a
|
||||
* block would be refused for.
|
||||
*/
|
||||
public function savePreset(string $name, ?array $style, StyleCompiler $compiler): void
|
||||
{
|
||||
$name = trim($name);
|
||||
if ('' === $name || mb_strlen($name) > 60) {
|
||||
throw new InvalidPage('Eine Vorlage braucht einen Namen mit höchstens 60 Zeichen.');
|
||||
}
|
||||
$key = trim(preg_replace('/[^a-z0-9]+/', '-', mb_strtolower($name, 'UTF-8')) ?? '', '-');
|
||||
if ('' === $key) {
|
||||
throw new InvalidPage('Aus diesem Namen lässt sich kein Schlüssel bilden.');
|
||||
}
|
||||
if (null !== $style) {
|
||||
$compiler->validate($style);
|
||||
}
|
||||
$theme = $this->load();
|
||||
$presets = array_values(array_filter($theme['presets'], static fn (array $p): bool => $p['key'] !== $key));
|
||||
if (null !== $style) {
|
||||
if (count($presets) >= self::MAX_PRESETS) {
|
||||
throw new InvalidPage(sprintf('Mehr als %d Vorlagen sind nicht vorgesehen.', self::MAX_PRESETS));
|
||||
}
|
||||
$presets[] = ['key' => $key, 'name' => $name, 'style' => $style];
|
||||
}
|
||||
usort($presets, static fn (array $a, array $b): int => strcmp($a['name'], $b['name']));
|
||||
$this->db->executeStatement('UPDATE website SET theme = ? WHERE id = ?', [
|
||||
json_encode([...$theme, 'presets' => $presets], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
|
||||
'default',
|
||||
]);
|
||||
}
|
||||
|
||||
/** The stylesheet every page carries: palette variables, font variables and base typography. */
|
||||
public function css(array $theme): string
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user