Tabs als fortschreitende Verbesserung (Dokumentschema v7)
Auf Entscheidung von Umut werden Tabs jetzt mit JavaScript umgesetzt. Die Regel "der Seiteninhalt braucht kein Browser-JavaScript" bleibt dabei eingehalten: der Server liefert eine Ankerliste und alle Panels mit eigenen Überschriften, versteckt nichts, und ohne Skript ist jeder Inhalt lesbar und über die Tastatur erreichbar. public/assets/tabs.js ergänzt tablist/tab/tabpanel, Auswahl per Pfeiltasten, Home und Ende sowie aria-selected und wählt bei einem passenden Sprungziel das zugehörige Panel. Die strenge Richtlinie bleibt Voreinstellung: öffentliche Seiten senden weiterhin script-src 'none'. Nur eine Seite mit Tab-Block sendet script-src 'self' und bindet die eine externe Datei ein; Inline-Skript ist nie erlaubt. Geprüft: 109 PHP-Tests / 395 Assertions unter PHP 8.2.12 und 8.4.22, 14 JavaScript-Modelltests, Lint/Format/Container. Das Skript selbst wurde nicht ausgeführt: Tastaturbedienung und ARIA-Verhalten sind ohne Browserlauf ungeprüft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8600384f1a
commit
c6ec00a906
17 files changed
+279
-26
No files matched your search
@@ -11,11 +11,13 @@ final class DocumentValidator
|
||||
/** Icons ship as inline SVG from this fixed list; no icon font and no external request. */
|
||||
public const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check'];
|
||||
public const SPACER_SIZES = ['small', 'medium', 'large', 'huge'];
|
||||
/** Schema v7: tabs, the one pattern that needs a script to become a real widget. */
|
||||
public const V7_TYPES = ['tabs', 'tabitem'];
|
||||
|
||||
public function validate(array $document): void
|
||||
{
|
||||
$schemaVersion = $document['schemaVersion'] ?? null;
|
||||
$this->require(in_array($schemaVersion, [1, 2, 3, 4, 5, 6], true), 'Unsupported document schema.');
|
||||
$this->require(in_array($schemaVersion, [1, 2, 3, 4, 5, 6, 7], true), 'Unsupported document schema.');
|
||||
$this->text($document['title'] ?? null, 160);
|
||||
$this->text($document['description'] ?? null, 320);
|
||||
$this->require(is_array($document['blocks'] ?? null) && array_is_list($document['blocks']), 'Blocks must be a list.');
|
||||
@@ -40,10 +42,11 @@ final class DocumentValidator
|
||||
// page: a deactivated plugin must never make stored content unreadable. Nothing from
|
||||
// props is rendered for such a block, so unvalidated values never reach the output.
|
||||
$plugin = $schemaVersion >= 4 && is_string($type) && 1 === preg_match('/^[a-z][a-z0-9]{1,30}\.[a-z][a-z0-9-]{1,40}$/D', $type);
|
||||
$this->require(in_array($type, ['section', 'row', 'column', 'heading', 'text', 'image', 'button'], true) || ($schemaVersion >= 3 && 'gallery' === $type) || ($schemaVersion >= 5 && 'postlist' === $type) || ($schemaVersion >= 6 && in_array($type, self::V6_TYPES, true)) || $plugin, 'Unknown block type. Preserve source until its plugin is available.');
|
||||
$this->require(in_array($type, ['section', 'row', 'column', 'heading', 'text', 'image', 'button'], true) || ($schemaVersion >= 3 && 'gallery' === $type) || ($schemaVersion >= 5 && 'postlist' === $type) || ($schemaVersion >= 6 && in_array($type, self::V6_TYPES, true)) || ($schemaVersion >= 7 && in_array($type, self::V7_TYPES, true)) || $plugin, 'Unknown block type. Preserve source until its plugin is available.');
|
||||
// An accordion entry only ever renders inside an accordion; at the top level or anywhere
|
||||
// else it would produce a details element without its group.
|
||||
$this->require('accordionitem' !== $type || 'accordion' === $parentType, 'An accordion entry belongs into an accordion.');
|
||||
$this->require('tabitem' !== $type || 'tabs' === $parentType, 'A tab belongs into a tab group.');
|
||||
$props = $block['props'] ?? [];
|
||||
$layout = $block['layout'] ?? [];
|
||||
$this->require(is_array($props) && is_array($layout), 'Props and layout must be objects.');
|
||||
@@ -127,6 +130,9 @@ final class DocumentValidator
|
||||
$this->text($props['title'] ?? null, 160);
|
||||
$this->require(is_bool($props['open'] ?? false), 'The open state is a boolean.');
|
||||
}
|
||||
if ('tabitem' === $type) {
|
||||
$this->text($props['title'] ?? null, 160);
|
||||
}
|
||||
if ('image' === $type) {
|
||||
$this->url($props['src'] ?? null);
|
||||
$this->require(is_bool($props['decorative'] ?? null), 'Image purpose is required.');
|
||||
@@ -160,10 +166,10 @@ final class DocumentValidator
|
||||
$children = $block['children'] ?? [];
|
||||
$this->require(is_array($children) && array_is_list($children), 'Children must be a list.');
|
||||
// A plugin block may nest content; keeping its children is part of preserving the page.
|
||||
$container = $plugin || in_array($type, ['section', 'row', 'column', 'gallery', 'logos', 'accordion', 'accordionitem'], true);
|
||||
$container = $plugin || in_array($type, ['section', 'row', 'column', 'gallery', 'logos', 'accordion', 'accordionitem', 'tabs', 'tabitem'], true);
|
||||
$this->require($container || [] === $children, 'Leaf blocks cannot contain children.');
|
||||
// Containers that only accept one kind of child, so the rendered markup stays valid.
|
||||
$only = ['gallery' => 'image', 'logos' => 'image', 'accordion' => 'accordionitem'];
|
||||
$only = ['gallery' => 'image', 'logos' => 'image', 'accordion' => 'accordionitem', 'tabs' => 'tabitem'];
|
||||
foreach ($children as $child) {
|
||||
if (isset($only[$type])) {
|
||||
$this->require(is_array($child) && $only[$type] === ($child['type'] ?? null), sprintf('A %s only contains %s blocks.', $type, $only[$type]));
|
||||
|
||||
Reference in new issue
Block a user