Let a button carry a chosen icon before or after its text, and pick icons from tiles
Schema v25: a button takes an optional icon (any name from the shared set or "none") and an icon position (start or end). Without a choice it keeps the diagonal arrow behind the text, still with the button-arrow class, so stored pages and their own CSS look exactly as before. Thirteen everyday icons join the set (arrow down, chevron, calendar, message, send, download, document, play, search, heart, info, plus, person). Button, icon and icon card choose from a tile picker with a drawing per icon, grouped, keyboard navigable, instead of a select with English keys. icons.js holds the paths for the editor; a test keeps them equal to the Twig drawings and the validator list. ADR 108. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1753fa7e4
commit
88737a95a9
17 files changed
+463
-15
No files matched your search
@@ -1 +1 @@
|
||||
.button-designer{padding:16px 0;border-block:1px solid #dce4dc;margin-block:16px}.button-designer h3{margin:0 0 12px;font-size:15px}.button-state-previews{display:grid;gap:12px;padding:16px;background:#fff;border:1px solid #dce4dc;border-radius:12px;margin-bottom:16px}.button-state{display:grid;justify-items:start;gap:7px;min-width:0}.button-state>.field-help{margin:0}.button-preset-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:12px 0 16px}.button-preset{display:grid;justify-items:center;align-content:center;gap:9px;min-width:0;padding:12px 6px;background:#fff;border:1px solid #dce4dc;border-radius:10px;color:#203b34;cursor:pointer}.button-preset[aria-pressed=true]{border-color:#2c6b4f;box-shadow:inset 0 0 0 1px #2c6b4f;background:#f1f6ef}.button-preset:focus-visible{outline:3px solid #b55b12;outline-offset:2px}.button-preset .cms-button{font-size:12px!important;padding:6px 10px;min-height:34px;pointer-events:none}.button-preset-label{font-size:11px;line-height:1.4;text-align:center}.button-designer select{width:100%;margin-bottom:12px}.button-defaults{margin-block:24px;padding:24px;border:1px solid #dce4dc;border-radius:14px;min-width:0}.button-defaults legend{font-size:20px;font-weight:700;padding:0 8px}.button-defaults-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,.75fr);gap:28px}.button-defaults .fields{align-content:start}.button-defaults .button-preview-column{min-width:0}.button-defaults .button-preview-column h3{font-size:15px;margin:0 0 12px}.button-defaults .color-field{margin:0}.button-defaults .color-trigger{width:100%}@media(max-width:900px){.button-defaults-layout{grid-template-columns:1fr}.button-defaults{padding:16px}}
|
||||
.button-designer{padding:16px 0;border-block:1px solid #dce4dc;margin-block:16px}.button-designer h3{margin:0 0 12px;font-size:15px}.button-state-previews{display:grid;gap:12px;padding:16px;background:#fff;border:1px solid #dce4dc;border-radius:12px;margin-bottom:16px}.button-state{display:grid;justify-items:start;gap:7px;min-width:0}.button-state>.field-help{margin:0}.button-preset-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:12px 0 16px}.button-preset{display:grid;justify-items:center;align-content:center;gap:9px;min-width:0;padding:12px 6px;background:#fff;border:1px solid #dce4dc;border-radius:10px;color:#203b34;cursor:pointer}.button-preset[aria-pressed=true]{border-color:#2c6b4f;box-shadow:inset 0 0 0 1px #2c6b4f;background:#f1f6ef}.button-preset:focus-visible{outline:3px solid #b55b12;outline-offset:2px}.button-preset .cms-button{font-size:12px!important;padding:6px 10px;min-height:34px;pointer-events:none}.button-preset-label{font-size:11px;line-height:1.4;text-align:center}.button-designer select{width:100%;margin-bottom:12px}.button-defaults{margin-block:24px;padding:24px;border:1px solid #dce4dc;border-radius:14px;min-width:0}.button-defaults legend{font-size:20px;font-weight:700;padding:0 8px}.button-defaults-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,.75fr);gap:28px}.button-defaults .fields{align-content:start}.button-defaults .button-preview-column{min-width:0}.button-defaults .button-preview-column h3{font-size:15px;margin:0 0 12px}.button-defaults .color-field{margin:0}.button-defaults .color-trigger{width:100%}@media(max-width:900px){.button-defaults-layout{grid-template-columns:1fr}.button-defaults{padding:16px}}.icon-picker{margin:4px 0 16px}.icon-picker-label{margin:0 0 6px;font-size:13px;font-weight:700;color:#203b34}.icon-group-title{margin:10px 0 6px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:#5b6b64}.icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(40px,1fr));gap:6px}.icon-tile{display:grid;place-items:center;aspect-ratio:1;min-height:40px;padding:0;background:#fff;border:1px solid #dce4dc;border-radius:9px;color:#203b34;cursor:pointer;transition:border-color .15s,background-color .15s}.icon-tile svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.icon-tile:hover{border-color:#8fb39f;background:#f7faf5}.icon-tile[aria-pressed=true]{border-color:#2c6b4f;box-shadow:inset 0 0 0 1px #2c6b4f;background:#e8f2e4;color:#1d4d37}.icon-tile:focus-visible{outline:3px solid #b55b12;outline-offset:2px}.icon-tile-none{grid-column:span 2;aspect-ratio:auto}.icon-tile-none::after{content:"Kein Symbol";font-size:12px;font-weight:600}.icon-tile-empty{display:none}.icon-position{margin:0 0 16px}.segmented{display:grid;grid-template-columns:1fr 1fr;border:1px solid #dce4dc;border-radius:9px;overflow:hidden}.segmented button{min-height:38px;border:0;background:#fff;color:#203b34;font-size:13px;font-weight:600;cursor:pointer}.segmented button+button{border-left:1px solid #dce4dc}.segmented button[aria-pressed=true]{background:#2c6b4f;color:#fff}.segmented button:focus-visible{outline:3px solid #b55b12;outline-offset:-3px}
|
||||
@@ -174,6 +174,7 @@ export function repairSchema(document) {
|
||||
if (block.anchor) upgradeDocument(document, 21);
|
||||
if ('collapse' in props) upgradeDocument(document, 22);
|
||||
if ('faq' in props) upgradeDocument(document, 23);
|
||||
if (block.type === 'button' && ('icon' in props || 'iconPosition' in props)) upgradeDocument(document, 25);
|
||||
walk(block.children);
|
||||
}
|
||||
};
|
||||
@@ -342,7 +343,7 @@ export const devices = { desktop: 'Desktop', tablet: 'Tablet', mobile: 'Mobil' }
|
||||
// blieb bei 12 stehen, während Bausteine schon v13 bis v15 verlangten: in einem aktuellen Dokument
|
||||
// liess sich dann gar nichts mehr einfügen, und ein Symbol, ein Slider oder ein externer Inhalt
|
||||
// nirgends. Eine Zahl, an einer Stelle.
|
||||
export const SCHEMA_VERSION = 24;
|
||||
export const SCHEMA_VERSION = 25;
|
||||
|
||||
export const VISIBILITY = { all: 'Auf allen Geräten', desktop: 'Nur Desktop', tablet: 'Nur Tablet', mobile: 'Nur Mobil', 'hide-desktop': 'Auf Desktop ausblenden', 'hide-tablet': 'Auf Tablet ausblenden', 'hide-mobile': 'Auf Mobil ausblenden', hidden: 'Überall ausblenden' };
|
||||
|
||||
|
||||
+37
-4
@@ -11,8 +11,8 @@ import { initMotion } from './motion.js';
|
||||
import { initSliders } from './slider.js';
|
||||
import { VISIBILITY, setVisibility, registerPlugins, pluginDefinition, PLUGIN_PRESETS, repairSchema } from './editor-model.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', 'people', 'music', 'home'];
|
||||
// Symbole, ihre deutschen Namen und die Kachelauswahl. Die Liste spiegelt DocumentValidator::ICONS.
|
||||
import { renderIconPicker, BUTTON_ICON_GROUPS, ALL_ICON_GROUPS } from './icons.js';
|
||||
const ICON_SIZES = [12, 16, 20, 24, 32, 40, 48, 56, 64, 80, 96, 120, 160, 200, 240];
|
||||
// Mirrors SocialProfiles::NETWORKS; the addresses themselves live in the settings.
|
||||
const SOCIAL_NETWORKS = { facebook: 'Facebook', instagram: 'Instagram', linkedin: 'LinkedIn', youtube: 'YouTube', tiktok: 'TikTok', x: 'X', threads: 'Threads', pinterest: 'Pinterest', xing: 'Xing', whatsapp: 'WhatsApp', google: 'Google-Unternehmensprofil' };
|
||||
@@ -346,6 +346,21 @@ function renderContent(block, panel) {
|
||||
if (key === 'href' && !options.plugin) field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true, container });
|
||||
}
|
||||
};
|
||||
// Symbolwahl als Kacheln mit Vorschau. Nach der Wahl wird das Panel neu gezeichnet, damit die
|
||||
// Markierung stimmt, und der Fokus bleibt auf der gewählten Kachel.
|
||||
const choose = (key, next, version) => {
|
||||
history.change(doc => {
|
||||
const current = locate(doc.blocks, selected)?.block;
|
||||
if (!current) return;
|
||||
(current.props ??= {})[key] = next;
|
||||
if (version) upgradeDocument(doc, version);
|
||||
}, null);
|
||||
renderProperties(); renderTree(); controls(); schedulePreview();
|
||||
document.querySelector(key === 'iconPosition' ? `.icon-position [data-position="${next}"]` : `.icon-tile[data-icon="${next}"]`)?.focus({ preventScroll: true });
|
||||
};
|
||||
const iconField = (label, value, groups, options = {}) => panel.append(renderIconPicker({
|
||||
label, value, groups, none: options.none, onChoose: name => choose('icon', name, options.version),
|
||||
}));
|
||||
if (!block) {
|
||||
field('Seitentitel', 'title', history.document.title, { maxLength: 160 });
|
||||
field('Beschreibung für Suchmaschinen', 'description', history.document.description, { multiline: true, maxLength: 320 });
|
||||
@@ -385,6 +400,24 @@ function renderContent(block, panel) {
|
||||
panel.append(label, rich.element, help);
|
||||
}
|
||||
if (['heading', 'button'].includes(block.type)) field(block.type === 'button' ? 'Beschriftung' : 'Text', 'text', block.props.text, { multiline: block.type !== 'button' });
|
||||
if (block.type === 'button') {
|
||||
// Ohne eigene Wahl trägt ein Button den Pfeil, den er vor v25 immer hatte.
|
||||
const icon = block.props.icon ?? 'arrow-up-right';
|
||||
iconField('Symbol', icon, BUTTON_ICON_GROUPS, { none: 'Kein Symbol', version: 25 });
|
||||
if (icon !== 'none') {
|
||||
const position = block.props.iconPosition === 'start' ? 'start' : 'end';
|
||||
const wrap = document.createElement('div'); wrap.className = 'icon-position';
|
||||
const caption = document.createElement('p'); caption.className = 'icon-picker-label'; caption.id = 'icon-position-label'; caption.textContent = 'Symbol steht';
|
||||
const group = document.createElement('div'); group.className = 'segmented'; group.setAttribute('role', 'group'); group.setAttribute('aria-labelledby', caption.id);
|
||||
for (const [value, text] of [['start', 'Vor dem Text'], ['end', 'Nach dem Text']]) {
|
||||
const option = document.createElement('button'); option.type = 'button'; option.dataset.position = value; option.textContent = text;
|
||||
option.setAttribute('aria-pressed', String(value === position));
|
||||
option.addEventListener('click', () => { if (value !== position) choose('iconPosition', value, 25); });
|
||||
group.append(option);
|
||||
}
|
||||
wrap.append(caption, group); panel.append(wrap);
|
||||
}
|
||||
}
|
||||
if (block.type === 'button' && designSchema) buttonControls(block, panel);
|
||||
if (INLINE_TEXT[block.type]) {
|
||||
const inline = document.createElement('button');
|
||||
@@ -399,7 +432,7 @@ function renderContent(block, panel) {
|
||||
}
|
||||
if (block.type === 'heading') field('Überschriftenebene', 'level', block.props.level, { number: true, choices: { 1: 'H1 · Hauptüberschrift', 2: 'H2 · Abschnitt', 3: 'H3 · Unterabschnitt', 4: 'H4', 5: 'H5', 6: 'H6' }, help: 'Jede Seite benötigt genau eine H1.' });
|
||||
if (block.type === 'iconcard') {
|
||||
field('Symbol', 'icon', block.props.icon, { choices: Object.fromEntries(ICONS.map(name => [name, name])) });
|
||||
iconField('Symbol', block.props.icon, ALL_ICON_GROUPS);
|
||||
field('Titel', 'title', block.props.title, { maxLength: 120 });
|
||||
field('Text', 'text', block.props.text, { multiline: true, maxLength: 600 });
|
||||
field('Linkziel', 'href', block.props.href || '', { help: 'Optional. Lokaler Pfad, Sprungmarke oder HTTPS-Adresse, Telefon (tel:+49…) oder E-Mail (mailto:…) – oder ein PDF.' });
|
||||
@@ -434,7 +467,7 @@ function renderContent(block, panel) {
|
||||
}
|
||||
if (block.type === 'partialref') field('Sektion', 'partial', block.props.partial, { ...pick('sections', block.props.partial), help: 'Die Sektion wird verknüpft, nicht kopiert: eine Änderung dort wirkt auf allen Seiten, die sie einbinden. Angelegt und bearbeitet wird sie unter „Bereiche“.' });
|
||||
if (block.type === 'icon') {
|
||||
field('Symbol', 'icon', block.props.icon, { choices: Object.fromEntries(ICONS.map(name => [name, name])) });
|
||||
iconField('Symbol', block.props.icon, ALL_ICON_GROUPS);
|
||||
// number, so the value is stored as a number: the server wants an int, not "48".
|
||||
field('Größe', 'size', String(block.props.size), { number: true, choices: Object.fromEntries(ICON_SIZES.map(size => [String(size), `${size} px`])), help: 'Feste Stufen, weil die Größe eine Klasse ist – ein freier Wert hätte keine und das Symbol wäre unsichtbar.' });
|
||||
field('Nur Dekoration', 'decorative', block.props.decorative, { checkbox: true, help: 'Dekorative Symbole werden Screenreadern verschwiegen. Trägt das Symbol eine Aussage, nimm das Häkchen weg und beschrifte es.' });
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// Die Zeichnungen der Symbole für die Auswahl im Editor. Die Website zeichnet dieselben Pfade aus
|
||||
// templates/blocks/icon.html.twig; ein Test hält beide Listen und DocumentValidator::ICONS gleich.
|
||||
export const ICON_PATHS = {
|
||||
app: 'M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm4 17h2',
|
||||
web: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 0c2.5 2.4 3.8 5.4 3.8 9s-1.3 6.6-3.8 9m0-18C9.5 5.4 8.2 8.4 8.2 12s1.3 6.6 3.8 9M3.2 9h17.6M3.2 15h17.6',
|
||||
design: 'M4 20h16M6 16 18 4l3 3L9 19l-4 1Z',
|
||||
network: 'M5 3h14v5H5Zm0 13h14v5H5Zm7-8v8M8 8v3h8V8',
|
||||
hardware: 'M4 5h16v11H4Zm4 15h8m-4-4v4',
|
||||
shop: 'M4 7h16l-1.4 11.2a2 2 0 0 1-2 1.8H7.4a2 2 0 0 1-2-1.8Zm4 0a4 4 0 0 1 8 0',
|
||||
support: 'M12 3a7 7 0 0 0-7 7v5m14-5a7 7 0 0 0-7-7m7 7v5m-14 0a2 2 0 0 0 2 2h1v-5H5Zm14 0a2 2 0 0 1-2 2h-1v-5h3Z',
|
||||
shield: 'M12 3 5 6v6c0 4.4 2.9 7.9 7 9 4.1-1.1 7-4.6 7-9V6Zm-2.5 9 2 2 4-4.5',
|
||||
clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5.5l3.5 2',
|
||||
star: 'm12 3 2.7 5.9 6.3.7-4.7 4.3 1.3 6.1L12 17l-5.6 3 1.3-6.1L3 9.6l6.3-.7Z',
|
||||
phone: 'M6 3h4l2 5-2.5 1.5a12 12 0 0 0 5 5L16 12l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 4 5a2 2 0 0 1 2-2Z',
|
||||
mail: 'M3 6h18v12H3Zm0 1 9 7 9-7',
|
||||
pin: 'M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z',
|
||||
check: 'm4 12.5 5.5 5.5L20 6',
|
||||
people: 'M9.5 11.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM2.5 20v-1c0-3.3 3-5 7-5s7 1.7 7 5v1M16.5 5.3a3.2 3.2 0 0 1 0 6.4M18 14.4c2.1.7 3.5 2.1 3.5 4.1V20',
|
||||
music: 'M10 18V5.5L20 3v12.5M10 9.5 20 7M10 18a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0ZM20 15.5a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0Z',
|
||||
home: 'm3 11.5 9-7.5 9 7.5M5.5 10v10h13V10M10 20v-6.5h4V20',
|
||||
'arrow-right': 'M4 12h15.5m-6.5-6.5 6.5 6.5-6.5 6.5',
|
||||
'arrow-left': 'M20 12H4.5M11 5.5 4.5 12 11 18.5',
|
||||
'arrow-up-right': 'M6.5 17.5 17.5 6.5M8.5 6.5h9v9',
|
||||
'arrow-down': 'M12 4v15.5m-6.5-6.5 6.5 6.5 6.5-6.5',
|
||||
'chevron-right': 'm9 5.5 6.5 6.5L9 18.5',
|
||||
calendar: 'M4 6h16v14H4Zm0 5h16M8 3.5v4m8-4v4',
|
||||
chat: 'M4 5h16v11H9.5L5 19.5V16H4Z',
|
||||
send: 'M21 3 10 14M21 3l-6.5 18-4.5-7-7-4.5Z',
|
||||
download: 'M12 3.5v12m-5-5 5 5 5-5M4.5 20h15',
|
||||
document: 'M6 3h8l4 4v14H6Zm8 0v4h4M9 12h6m-6 4h6',
|
||||
play: 'M8 5.5v13l10-6.5Z',
|
||||
search: 'M10.5 4a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm4.8 11.3L20 20',
|
||||
heart: 'M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 7.4 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10Z',
|
||||
info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 8v5.5m0-9v.5',
|
||||
plus: 'M12 5v14M5 12h14',
|
||||
user: 'M12 11.5a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4.5 20.5c0-3.9 3.4-6 7.5-6s7.5 2.1 7.5 6',
|
||||
};
|
||||
|
||||
export const ICON_LABELS = {
|
||||
app: 'Smartphone', web: 'Globus', design: 'Stift', network: 'Netzwerk', hardware: 'Bildschirm',
|
||||
shop: 'Einkaufstasche', support: 'Kopfhörer', shield: 'Schutz', clock: 'Uhr', star: 'Stern',
|
||||
phone: 'Telefon', mail: 'E-Mail', pin: 'Ort', check: 'Häkchen', people: 'Personen', music: 'Musik',
|
||||
home: 'Haus', 'arrow-right': 'Pfeil nach rechts', 'arrow-left': 'Pfeil nach links',
|
||||
'arrow-up-right': 'Pfeil schräg', 'arrow-down': 'Pfeil nach unten', 'chevron-right': 'Winkel',
|
||||
calendar: 'Kalender', chat: 'Nachricht', send: 'Senden', download: 'Download', document: 'Dokument',
|
||||
play: 'Abspielen', search: 'Suche', heart: 'Herz', info: 'Info', plus: 'Plus', user: 'Person',
|
||||
};
|
||||
|
||||
/** Die Auswahl am Button: die Symbole, die man an einem Knopf wirklich braucht, nach Zweck geordnet. */
|
||||
export const BUTTON_ICON_GROUPS = [
|
||||
['Pfeile', ['arrow-up-right', 'arrow-right', 'chevron-right', 'arrow-down', 'arrow-left']],
|
||||
['Kontakt', ['phone', 'mail', 'chat', 'send', 'calendar', 'pin']],
|
||||
['Allgemein', ['download', 'document', 'play', 'search', 'check', 'star', 'heart', 'info', 'plus', 'user', 'home', 'shop', 'clock']],
|
||||
];
|
||||
|
||||
/** Alle Symbole für Symbol und Symbolkarte, in derselben Ordnung plus die übrigen. */
|
||||
export const ALL_ICON_GROUPS = [
|
||||
...BUTTON_ICON_GROUPS,
|
||||
['Weitere', ['app', 'web', 'design', 'network', 'hardware', 'support', 'shield', 'people', 'music']],
|
||||
];
|
||||
|
||||
const svgNS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
export function iconSvg(name) {
|
||||
const svg = document.createElementNS(svgNS, 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
svg.setAttribute('focusable', 'false');
|
||||
const path = document.createElementNS(svgNS, 'path');
|
||||
path.setAttribute('d', ICON_PATHS[name] ?? '');
|
||||
svg.append(path);
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Kachelauswahl statt einer Liste aus Namen: jedes Symbol ist ein Knopf mit seiner Zeichnung,
|
||||
* der gewählte ist markiert. Pfeiltasten wandern innerhalb der Gruppe, Tab verlässt die Auswahl.
|
||||
*
|
||||
* @param {{ label: string, value: string, groups: Array<[string, string[]]>, none?: string, onChoose: (name: string) => void }} options
|
||||
*/
|
||||
export function renderIconPicker({ label, value, groups, none, onChoose }) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'icon-picker';
|
||||
const heading = document.createElement('p');
|
||||
heading.className = 'icon-picker-label';
|
||||
heading.id = `icon-picker-${Math.random().toString(36).slice(2, 8)}`;
|
||||
heading.textContent = label;
|
||||
wrap.append(heading);
|
||||
const tiles = [];
|
||||
const tile = name => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = name === 'none' ? 'icon-tile icon-tile-none' : 'icon-tile';
|
||||
button.dataset.icon = name;
|
||||
const text = name === 'none' ? none : ICON_LABELS[name];
|
||||
button.title = text;
|
||||
button.setAttribute('aria-label', text);
|
||||
button.setAttribute('aria-pressed', String(name === value));
|
||||
button.tabIndex = name === value ? 0 : -1;
|
||||
if (name === 'none') {
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'icon-tile-empty';
|
||||
mark.setAttribute('aria-hidden', 'true');
|
||||
button.append(mark);
|
||||
} else button.append(iconSvg(name));
|
||||
button.addEventListener('click', () => onChoose(name));
|
||||
tiles.push(button);
|
||||
return button;
|
||||
};
|
||||
const group = document.createElement('div');
|
||||
group.setAttribute('role', 'group');
|
||||
group.setAttribute('aria-labelledby', heading.id);
|
||||
if (none) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'icon-grid';
|
||||
row.append(tile('none'));
|
||||
group.append(row);
|
||||
}
|
||||
for (const [title, names] of groups) {
|
||||
const caption = document.createElement('p');
|
||||
caption.className = 'icon-group-title';
|
||||
caption.textContent = title;
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'icon-grid';
|
||||
for (const name of names) grid.append(tile(name));
|
||||
group.append(caption, grid);
|
||||
}
|
||||
// Ohne gewählten Knopf in der Liste (etwa ein Symbol außerhalb der Gruppen) bleibt der erste
|
||||
// erreichbar, sonst käme man mit Tab gar nicht hinein.
|
||||
if (!tiles.some(button => button.tabIndex === 0)) tiles[0].tabIndex = 0;
|
||||
group.addEventListener('keydown', event => {
|
||||
const index = tiles.indexOf(document.activeElement);
|
||||
if (index < 0) return;
|
||||
const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];
|
||||
const target = event.key === 'Home' ? 0 : event.key === 'End' ? tiles.length - 1 : step ? (index + step + tiles.length) % tiles.length : null;
|
||||
if (target === null) return;
|
||||
event.preventDefault();
|
||||
tiles[index].tabIndex = -1;
|
||||
tiles[target].tabIndex = 0;
|
||||
tiles[target].focus();
|
||||
});
|
||||
wrap.append(group);
|
||||
return wrap;
|
||||
}
|
||||
@@ -47,8 +47,8 @@ p{hyphens:auto}
|
||||
/* Der Pfeil am Button und in der Beitragsnavigation ist dasselbe Symbol wie ueberall sonst -
|
||||
frueher stand dort ein Schriftzeichen, das je nach Schrift anders aussah. Er waechst mit der
|
||||
Schrift des Knopfs, statt eine eigene Groesse zu haben. */
|
||||
.button-arrow,.nav-arrow{display:inline-flex;align-items:center}
|
||||
.button-arrow>svg,.nav-arrow>svg{width:1.15em;height:1.15em}
|
||||
.button-icon,.nav-arrow{display:inline-flex;align-items:center}
|
||||
.button-icon>svg,.nav-arrow>svg{width:1.15em;height:1.15em}
|
||||
.block-iconcard{display:grid;gap:12px;padding:26px;border:1px solid currentColor;border-radius:12px}
|
||||
.block-iconcard + .block-iconcard,.block-quote + .block-quote,.block-cta + .block-cta,.block-iconcard + .block-quote,.block-quote + .block-iconcard{margin-top:18px}
|
||||
/* Gestapelt brauchen Karten den Abstand; nebeneinander in einer Zeile macht er die zweite bis
|
||||
@@ -269,7 +269,7 @@ body.consent-open{padding-bottom:320px}
|
||||
.block-icon .block-icon{color:inherit}
|
||||
/* Der Pfeil im Knopf ist dasselbe Symbol und nahm deshalb die Akzentfarbe statt der Schriftfarbe
|
||||
des Knopfs: grün auf Rot, auch beim Überfahren (Hüfner, 06.10.2026). */
|
||||
.button-arrow .block-icon{color:inherit}
|
||||
.button-icon .block-icon{color:inherit}
|
||||
.icon-size-12{width:12px;height:12px}.icon-size-16{width:16px;height:16px}.icon-size-20{width:20px;height:20px}
|
||||
.icon-size-24{width:24px;height:24px}.icon-size-32{width:32px;height:32px}.icon-size-40{width:40px;height:40px}
|
||||
.icon-size-48{width:48px;height:48px}.icon-size-56{width:56px;height:56px}.icon-size-64{width:64px;height:64px}
|
||||
|
||||
Reference in new issue
Block a user