Freie Gestaltung statt Tokenlisten (Dokumentschema v10)

Der bisherige Editor bot drei Farben, keine Schriftwahl und elf
Auswahllisten mit 44 festen Werten. ADR 008 hatte das mit Sicherheit
begründet - der Schluss war falsch: die Alternative zu beliebigem
CSS-Text ist nicht eine Auswahlliste, sondern ein typisiertes
Designmodell.

StyleSchema erklärt jede Eigenschaft einmal mit Typ, CSS-Entsprechung
und Grenzen. Validator, CSS-Erzeugung und Editorpanel lesen dieselbe
Definition, also kann der Editor nichts anbieten, was der Server
ablehnt. Rund fünfzig Eigenschaften in sieben Gruppen, je Block, je
Gerät und für den Hover-Zustand, mit Vererbung und einzelnem
Zurücksetzen.

Werte sind kurze Zeichenketten mit verankertem Muster, nie CSS-Text;
zusammengesetzte Deklarationen baut der Compiler selbst. Der Server
erzeugt daraus pro Seite ein blockgenaues Stylesheet, das über seinen
eigenen SHA-256-Hash erlaubt wird statt über unsafe-inline.

Theme unter /admin/theme: freie Farbpalette als @schlüssel, Schriften
aus Systemschrift oder Google Fonts, Grundtypografie mit modularer
Überschriftenskala. Google Fonts auf ausdrückliche Nutzerentscheidung;
die Richtlinie öffnet die Hosts nur, wenn wirklich eine eingestellt
ist.

Alte Tokenwerte aus v2 bis v9 bleiben gültig und im Editor unter
"Klassische Werte dieser Seite" bearbeitbar.

Geprüft: 154 PHP-Tests / 563 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, Lint/Format/Container. Im Browser wurde nur
der Aufbau des Panels bestätigt, kein Gestaltungsvorgang von Hand
ausgeführt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 13:04:54 +02:00
1 parent a40c2bb27e
commit abf0bd2c6b
24 files changed
+1591 -21

No files matched your search

+40 -2
View File
@@ -1,5 +1,6 @@
import { EditorHistory, labels, icons, blockLabel, blockIcon, isPluginBlock, locate, clone, isContainer, canContain, insertBlock, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, setEffect, insertMedia, upgradeDocument } from './editor-model.js';
import { EditorHistory, labels, icons, blockLabel, blockIcon, isPluginBlock, setStyle, effectiveStyle, locate, clone, isContainer, canContain, insertBlock, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, setEffect, insertMedia, upgradeDocument } from './editor-model.js';
import { openMedia } from './media.js';
import { createDesignPanel } from './design-panel.js';
// Mirrors DocumentValidator::ICONS; the server rejects anything outside this list.
const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check'];
@@ -9,7 +10,7 @@ const $ = selector => document.querySelector(selector);
const api = `/admin/api/pages/${encodeURIComponent(root.dataset.pageId)}`;
let history, selected = null, version, revisionId, publishedRevisionId, csrfToken;
let busy = false, conflict = false, previewTimer, previewController, previewSequence = 0;
let device = 'desktop', propertyPanel = 'content', designTokens;
let device = 'desktop', propertyPanel = 'content', designTokens, designSchema, designPanel;
const collapsed = new Set();
function notify(message = '', recover = false) {
@@ -235,6 +236,24 @@ function renderProperties() {
}
function renderDesign(block, panel) {
if (designSchema && designPanel) {
designPanel(panel);
} else {
panel.append(Object.assign(document.createElement('p'), { className: 'panel-note', textContent: 'Gestaltungsoptionen werden geladen …' }));
}
const hasLegacy = Boolean(block.layout?.design) || Boolean(block.layout?.effects);
if (!hasLegacy) return;
const details = document.createElement('details'); details.className = 'design-group';
const summary = document.createElement('summary'); summary.textContent = 'Klassische Werte dieser Seite';
const body = document.createElement('div'); body.className = 'design-group-body';
details.append(summary, body);
renderLegacyDesign(block, body);
panel.append(details);
}
function renderLegacyDesign(block, panel) {
// Token based values from documents written before schema v10. Shown only while a block
// still carries them, so older pages stay editable without cluttering the new panel.
const hint = document.createElement('p'); hint.className = 'design-scope';
hint.textContent = `${devices[device]} · ${device === 'desktop' ? 'Basis für alle Geräte' : device === 'tablet' ? 'bis 1024 px; gilt auch für Mobil' : 'bis 650 px'}`;
const help = document.createElement('p'); help.className = 'field-help'; help.textContent = 'Oben die Vorschaugröße wählen. Ohne eigenen Wert gilt die Einstellung des größeren Geräts oder des Themes.';
@@ -338,6 +357,24 @@ function changed() {
renderTree(); renderProperties(); controls(); schedulePreview();
}
async function loadDesignSchema() {
try {
const response = await fetch('/admin/api/design', { credentials: 'same-origin' });
if (!response.ok) return;
designSchema = await response.json();
designPanel = createDesignPanel({
schema: designSchema,
onChange: () => { controls(); renderTree(); schedulePreview(); renderProperties(); },
getBlock: () => locate(history.document.blocks, selected)?.block,
getDevice: () => device,
getSelection: () => selected,
getHistory: () => history,
});
} catch {
// The editor stays usable without it; the panel says so.
}
}
async function load() {
busy = true; controls();
try {
@@ -347,6 +384,7 @@ async function load() {
if (!response.ok) throw new Error('Designwerkzeuge konnten nicht geladen werden. Bitte erneut versuchen.');
designTokens = await response.json();
}
if (!designSchema) await loadDesignSchema();
history = new EditorHistory(data.document);
({ version, revisionId, publishedRevisionId, csrfToken } = data);
selected = null; propertyPanel = 'content'; collapsed.clear(); conflict = false; notify(); changed(); resizePreview();