/* * 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); }