Files
kantarciandClaude Fable 5.1 160c394c3f
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Rework forms, statistics, design and the sidebar foot in 1.15.0
Forms are an editor now: one card per field with add and remove, the
technical name derived from the label, options only for a choice, and a
new "Aussehen" section with a button label and four colours (field border
and background, button and button text) that travel to the page as one
rule per form inside the hashed style block. A live preview draws the
form with the site's default button and names the text contrast.

Statistics open with six figures (views against the same span before,
average per day, busiest day, pages, referred views, missing addresses),
a chart with grid lines and a title per bar, a weekday chart and ranked
lists with share bars. The design screen shows the palette as colour
cards with seven ready-made palettes, picks the base colours from the
palette, previews every font in itself and the typography as a sample.
The sidebar ends in an account card with role and a logout icon.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 07:59:30 +02:00

172 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Die Designseite: Palette, Schriften, Grundtypografie.
*
* Ohne Skript sind es Karten mit Eingaben; eine Farbe ohne Schlüssel und eine Schrift ohne
* Schlüssel werden nicht gespeichert, so lassen sie sich auch dann entfernen. Das Skript macht
* daraus die eigentliche Werkstatt: Karten hinzufügen und entfernen, fertige Paletten übernehmen,
* die Grundfarben aus der Palette wählen statt tippen, jede Schrift in sich selbst zeigen und die
* Grundtypografie als lesbare Probe.
*/
import { createColorControl } from './color-control.js';
const form = document.querySelector('[data-theme-editor]');
if (form) {
const palettes = JSON.parse(form.dataset.palettes), colorNames = JSON.parse(form.dataset.colorNames);
const colorList = form.querySelector('[data-color-list]'), colorTemplate = form.querySelector('[data-color-template]');
const fontList = form.querySelector('[data-font-list]'), fontTemplate = form.querySelector('[data-font-template]');
const colorCards = () => [...colorList.querySelectorAll('[data-color-card]')];
const fontCards = () => [...fontList.querySelectorAll('[data-font-card]')];
let nextColor = colorCards().length, nextFont = fontCards().length;
const value = (card, selector) => card.querySelector(selector).value.trim();
const palette = () => colorCards().flatMap(card => {
const key = value(card, '[data-color-key]').toLowerCase();
return key ? [{ key, name: value(card, '[data-color-name]') || key, value: card.querySelector('[data-color-value]').value }] : [];
});
const fonts = () => fontCards().flatMap(card => {
const key = value(card, '[data-font-key]').toLowerCase();
return key ? [{ key, name: value(card, '[data-font-name]') || key, family: value(card, '[data-font-family]'), fallback: card.querySelector('[data-font-fallback]').value }] : [];
});
const stack = font => font.family ? `"${font.family}", ${font.fallback}` : font.fallback;
const hex = raw => /^#[0-9a-f]{6}/i.test(raw || '') ? raw.slice(0, 7) : palette().find(color => `@${color.key}` === raw)?.value || '';
/* ---- Palette ---------------------------------------------------------------------- */
const refreshColors = () => {
colorCards().forEach((card, index) => {
const name = value(card, '[data-color-name]') || value(card, '[data-color-key]') || `Farbe ${index + 1}`;
card.querySelector('[data-color-hex]').textContent = card.querySelector('[data-color-value]').value;
card.querySelector('[data-color-value]').setAttribute('aria-label', `Farbwert von ${name}`);
const remove = card.querySelector('[data-remove-color]');
remove.hidden = false; remove.setAttribute('aria-label', `${name} entfernen`);
});
form.querySelector('[data-color-count]').textContent = String(palette().length);
};
const addColor = (values = {}) => {
colorList.insertAdjacentHTML('beforeend', colorTemplate.innerHTML.replaceAll('__n__', String(nextColor++)));
const card = colorList.lastElementChild; card.classList.remove('swatch-card-new');
for (const [selector, field] of [['[data-color-key]', 'key'], ['[data-color-name]', 'name'], ['[data-color-value]', 'value']]) {
if (values[field] !== undefined) card.querySelector(selector).value = values[field];
}
return card;
};
// Die leeren Karten braucht es nur ohne Skript: hier kommt jede Farbe über den Knopf.
const emptyColors = colorCards().filter(card => card.classList.contains('swatch-card-new'));
if (colorCards().length > emptyColors.length) emptyColors.forEach(card => card.remove());
const addColorButton = form.querySelector('[data-add-color]'); addColorButton.hidden = false;
addColorButton.addEventListener('click', () => {
const card = addColor(); refreshColors(); mountBaseColors(); card.querySelector('[data-color-name]').focus();
});
colorList.addEventListener('click', event => {
const button = event.target.closest('[data-remove-color]');
if (!button) return;
button.closest('[data-color-card]').remove(); refreshColors(); mountBaseColors(); renderType(); addColorButton.focus();
});
colorList.addEventListener('input', () => { refreshColors(); renderType(); });
colorList.addEventListener('change', () => { mountBaseColors(); renderType(); });
const presets = form.querySelector('[data-palette-presets]'), status = form.querySelector('[data-palette-status]');
presets.hidden = false;
presets.addEventListener('click', event => {
const button = event.target.closest('[data-palette-preset]');
if (!button) return;
const preset = palettes[Number(button.dataset.palettePreset)];
// Bekannte Schlüssel bekommen den neuen Wert, fehlende kommen dazu, eigene Farben bleiben.
for (const [key, color] of Object.entries(preset.colors)) {
const card = colorCards().find(candidate => value(candidate, '[data-color-key]').toLowerCase() === key);
if (card) card.querySelector('[data-color-value]').value = color;
else addColor({ key, name: colorNames[key] || key, value: color });
}
refreshColors(); mountBaseColors(); renderType();
status.textContent = `Palette „${preset.name}“ übernommen – sie gilt, sobald das Design gespeichert ist.`;
});
/* ---- Schriften -------------------------------------------------------------------- */
const applySample = card => {
const family = value(card, '[data-font-family]'), fallback = card.querySelector('[data-font-fallback]').value;
card.querySelector('[data-font-sample]').style.fontFamily = stack({ family, fallback });
const chosen = new Set([...card.querySelectorAll('[data-font-weight]')].filter(input => input.checked).map(input => input.value));
for (const weight of card.querySelectorAll('[data-font-weights] > span')) weight.hidden = !chosen.has(weight.className.replace('font-weight-', ''));
};
const syncFontSelects = () => {
const list = fonts();
for (const select of form.querySelectorAll('[data-font-select]')) {
const current = select.value, keep = select.querySelector('option[value="inherit"]');
select.replaceChildren(...(keep ? [keep] : []), ...list.map(font => new Option(font.name, font.key)));
select.value = list.some(font => font.key === current) || (keep && current === 'inherit') ? current : (keep ? 'inherit' : list[0]?.key ?? '');
}
};
const refreshFonts = () => {
fontCards().forEach((card, index) => {
applySample(card);
const name = value(card, '[data-font-name]') || value(card, '[data-font-key]') || `Schrift ${index + 1}`;
const remove = card.querySelector('[data-remove-font]');
remove.hidden = false; remove.setAttribute('aria-label', `${name} entfernen`);
});
form.querySelector('[data-font-count]').textContent = String(fonts().length);
syncFontSelects();
};
const emptyFonts = fontCards().filter(card => card.classList.contains('font-card-new'));
if (fontCards().length > emptyFonts.length) emptyFonts.forEach(card => card.remove());
const addFontButton = form.querySelector('[data-add-font]'); addFontButton.hidden = false;
addFontButton.addEventListener('click', () => {
fontList.insertAdjacentHTML('beforeend', fontTemplate.innerHTML.replaceAll('__n__', String(nextFont++)));
const card = fontList.lastElementChild; card.classList.remove('font-card-new');
refreshFonts(); card.querySelector('[data-font-name]').focus();
});
fontList.addEventListener('click', event => {
const button = event.target.closest('[data-remove-font]');
if (!button) return;
button.closest('[data-font-card]').remove(); refreshFonts(); renderType(); addFontButton.focus();
});
fontList.addEventListener('input', () => { refreshFonts(); renderType(); });
fontList.addEventListener('change', () => { refreshFonts(); renderType(); });
/* ---- Grundtypografie -------------------------------------------------------------- */
const preview = form.querySelector('[data-type-preview]');
const baseInputs = {};
const lengthPattern = /^(0|-?\d{1,5}(\.\d{1,3})?(px|rem|em|%|vw|vh|ch))$/;
function renderType() {
const list = fonts();
const family = key => { const font = list.find(entry => entry.key === key); return font ? stack(font) : 'inherit'; };
const raw = form.querySelector('#base-size').value.trim(), size = lengthPattern.test(raw) ? raw : '18px';
const line = form.querySelector('#base-line').value || '1.6', scale = parseFloat(form.querySelector('#base-scale').value) || 1.25;
preview.style.fontFamily = family(form.querySelector('#base-font').value);
preview.style.fontSize = size; preview.style.lineHeight = line;
const heading = family(form.querySelector('#base-heading-font').value);
for (const [selector, step] of [['[data-type-h1]', 4], ['[data-type-h2]', 3]]) {
const node = preview.querySelector(selector);
node.style.fontFamily = heading; node.style.fontSize = `calc(${size} * ${(scale ** step).toFixed(4)})`;
}
preview.style.color = hex(baseInputs.color?.value) || '#203b34';
preview.style.background = hex(baseInputs.backgroundColor?.value) || '#ffffff';
const link = preview.querySelector('[data-type-link]');
link.style.color = hex(baseInputs.linkColor?.value) || 'inherit';
link.style.textDecoration = form.querySelector('[data-base-underline]').checked ? 'underline' : 'none';
}
// Die drei Grundfarben als Palettenwahl. Die Palette kann sich unter ihnen ändern, deshalb
// werden die Bedienelemente bei jeder Änderung der Palette neu aufgebaut; der Wert bleibt.
function mountBaseColors() {
const colors = palette();
for (const input of form.querySelectorAll('[data-base-color]')) {
const key = input.name.slice(5, -1);
if (!input.dataset.controlId) {
input.dataset.controlId = input.id; input.type = 'hidden'; input.id += '-value'; baseInputs[key] = input;
}
const id = input.dataset.controlId, label = form.querySelector(`label[for="${id}"]`).textContent;
input.nextElementSibling?.classList.contains('color-control') && input.nextElementSibling.remove();
const control = createColorControl({
id, label, value: input.value, colors,
onChange: chosen => { input.value = chosen; renderType(); }, onComplete: () => {},
});
control.querySelector('.color-inherit').hidden = true;
input.after(control);
}
}
refreshColors(); refreshFonts(); mountBaseColors(); renderType();
form.addEventListener('input', renderType); form.addEventListener('change', renderType);
}