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
@@ -99,7 +99,7 @@ export const devices = { desktop: 'Desktop', tablet: 'Tablet', mobile: 'Mobil' }
|
||||
|
||||
// Called only inside a history transaction: all other fields and IDs stay untouched.
|
||||
export function upgradeDocument(document, version = 2) {
|
||||
if (![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(document.schemaVersion) || ![2, 3, 4, 5, 6, 7, 8, 9].includes(version)) throw new Error('Unbekanntes Dokumentschema.');
|
||||
if (![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].includes(document.schemaVersion) || ![2, 3, 4, 5, 6, 7, 8, 9, 10].includes(version)) throw new Error('Unbekanntes Dokumentschema.');
|
||||
document.schemaVersion = Math.max(document.schemaVersion, version);
|
||||
return document;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export function effectiveDesign(block, device, key) {
|
||||
}
|
||||
|
||||
export function setDesign(document, id, device, key, value) {
|
||||
if (![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(document.schemaVersion)) throw new Error('Unbekanntes Dokumentschema.');
|
||||
if (![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].includes(document.schemaVersion)) throw new Error('Unbekanntes Dokumentschema.');
|
||||
if (!Object.hasOwn(devices, device)) throw new Error('Unbekannte Vorschaugröße.');
|
||||
const block = locate(document.blocks, id)?.block;
|
||||
if (!block) return;
|
||||
@@ -222,3 +222,92 @@ export class EditorHistory {
|
||||
markSaved(document) { this.saved = JSON.stringify(document); this.mergeKey = null; }
|
||||
get dirty() { return JSON.stringify(this.document) !== this.saved; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Free block styles (document schema v10).
|
||||
//
|
||||
// A style is { device: { property: value, hover: { property: value } } }. Desktop is the base,
|
||||
// tablet and mobile override it. Everything mirrors StyleSchema on the server: the editor can only
|
||||
// offer what the server accepts, and a value the server would reject never reaches a document.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/** The value that actually applies on a device, plus the device it was inherited from. */
|
||||
export function effectiveStyle(block, device, key, side = null) {
|
||||
let value, source;
|
||||
for (const name of Object.keys(devices)) {
|
||||
const stored = block?.style?.[name]?.[key];
|
||||
const candidate = side === null ? stored : stored?.[side];
|
||||
if (candidate !== undefined) { value = candidate; source = name; }
|
||||
if (name === device) break;
|
||||
}
|
||||
return { value, source };
|
||||
}
|
||||
|
||||
export function hoverStyle(block, device, key) {
|
||||
return block?.style?.[device]?.hover?.[key];
|
||||
}
|
||||
|
||||
/** Removes empty objects so an untouched block never carries a style skeleton. */
|
||||
function prune(block) {
|
||||
for (const device of Object.keys(block.style ?? {})) {
|
||||
const values = block.style[device];
|
||||
for (const key of Object.keys(values)) {
|
||||
if (values[key] && typeof values[key] === 'object' && Object.keys(values[key]).length === 0) delete values[key];
|
||||
}
|
||||
if (Object.keys(values).length === 0) delete block.style[device];
|
||||
}
|
||||
if (block.style && Object.keys(block.style).length === 0) delete block.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets one style value. An empty value removes it, which makes the block fall back to the wider
|
||||
* device and finally to the theme. Called inside a history transaction like every other change.
|
||||
*/
|
||||
export function setStyle(document, id, device, key, value, { side = null, hover = false } = {}) {
|
||||
if (!Object.hasOwn(devices, device)) throw new Error('Unbekannte Vorschaugröße.');
|
||||
const block = locate(document.blocks, id)?.block;
|
||||
if (!block) return;
|
||||
const empty = value === '' || value === null || value === undefined;
|
||||
if (!empty) upgradeDocument(document, 10);
|
||||
|
||||
block.style ??= {};
|
||||
block.style[device] ??= {};
|
||||
let target = block.style[device];
|
||||
if (hover) { target.hover ??= {}; target = target.hover; }
|
||||
|
||||
if (side !== null) {
|
||||
if (empty) { if (target[key]) delete target[key][side]; }
|
||||
else { target[key] ??= {}; target[key][side] = value; }
|
||||
} else if (empty) {
|
||||
delete target[key];
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
prune(block);
|
||||
}
|
||||
|
||||
/** Drops every style of one device, so it inherits again. */
|
||||
export function resetStyleDevice(document, id, device) {
|
||||
const block = locate(document.blocks, id)?.block;
|
||||
if (!block?.style) return;
|
||||
delete block.style[device];
|
||||
prune(block);
|
||||
}
|
||||
|
||||
/** Drops the whole styling of a block. */
|
||||
export function clearStyle(document, id) {
|
||||
const block = locate(document.blocks, id)?.block;
|
||||
if (!block) return;
|
||||
delete block.style;
|
||||
}
|
||||
|
||||
/** Copies the styling of one block onto another, with fresh objects. */
|
||||
export function copyStyle(document, fromId, toId) {
|
||||
const source = locate(document.blocks, fromId)?.block;
|
||||
const target = locate(document.blocks, toId)?.block;
|
||||
if (!source || !target) return false;
|
||||
if (!source.style) { delete target.style; return true; }
|
||||
upgradeDocument(document, 10);
|
||||
target.style = structuredClone(source.style);
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user