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:
1 parent
a40c2bb27e
commit
abf0bd2c6b
24 files changed
+1591
-21
No files matched your search
+40
-2
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user