From e7bdd295aa6dd2e755e3a316f6e2eb8d33536768 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 6 Feb 2026 16:26:36 +0100 Subject: [PATCH 1/4] Redesign speisen block for DA-friendly 2-column content model Replace CSS-only 5-column grid with a JS-decorated 2-column Collection model that is editable in the DA editor. The decoration code extracts item numbers from and descriptions from , creating a 3-column visual layout (nr | name+desc | prices) from 2 authored columns. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ben Peter --- blocks/speisen/speisen.css | 36 ++++++++-------------- blocks/speisen/speisen.js | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/blocks/speisen/speisen.css b/blocks/speisen/speisen.css index 152cd48..4611423 100644 --- a/blocks/speisen/speisen.css +++ b/blocks/speisen/speisen.css @@ -1,32 +1,22 @@ -.speisen > div { - display: grid; - grid-template: - "nr name p2 p1" - "nr description p2 p1" / 2em auto auto 4em; - gap: 0; - grid-auto-flow: row; - margin-bottom: 8px; +/* Speisen (menu) block — 3-column grid: nr | name+desc | prices */ +.speisen > div { + display: grid; + grid-template-columns: 2em 1fr auto; + margin-bottom: 8px; + align-items: baseline; } -.speisen > div > div:nth-child(4) { - text-align: right; - grid-area: p2; +.speisen .item-info .item-desc { + font-size: smaller; + font-style: italic; } -.speisen > div > div:nth-child(5) { - text-align: right; - padding-left: 8px; - grid-area: p1; -} - -.speisen > div > div:nth-child(3) { - grid-area: description; - font-size: smaller; - font-style: italic; - text-align: left; +.speisen .item-prices { + text-align: right; + padding-left: 8px; } /* reset p margins added by aem.js wrapTextNodes */ .speisen p { - margin: 0; + margin: 0; } diff --git a/blocks/speisen/speisen.js b/blocks/speisen/speisen.js index e69de29..61109b2 100644 --- a/blocks/speisen/speisen.js +++ b/blocks/speisen/speisen.js @@ -0,0 +1,62 @@ +/** + * Decorates the speisen (menu) block. + * + * Transforms a 2-column authored structure (item info | prices) + * into a 3-column visual layout (nr | name+desc | prices). + * + * Content model (Collection): + * Col 1: **Nr** Item Name *optional description* + * Col 2: Price — or multi-line for size variants (e.g. "klein 3,30 €\ngroß 4,40 €") + */ +export default function decorate(block) { + [...block.children].forEach((row) => { + const cols = [...row.children]; + if (cols.length < 2) return; + + const [info, prices] = cols; + + // Extract item number from a leading that matches Nr pattern + const firstP = info.querySelector('p'); + const container = firstP || info; + const firstEl = container.firstElementChild; + let nr = ''; + + if (firstEl?.tagName === 'STRONG') { + const text = firstEl.textContent.trim(); + if (/^[A-Za-z]?\d+[a-z]?$/.test(text)) { + nr = text; + firstEl.remove(); + // Remove leading whitespace left after the number + const next = container.firstChild; + if (next?.nodeType === Node.TEXT_NODE) { + next.textContent = next.textContent.replace(/^\s+/, ''); + } + } + } + + // Move description into its own paragraph below the name + const em = container.querySelector('em'); + if (em) { + const descP = document.createElement('p'); + descP.className = 'item-desc'; + descP.textContent = em.textContent; + container.after(descP); + em.remove(); + // Trim trailing whitespace from the name paragraph + container.normalize(); + if (container.lastChild?.nodeType === Node.TEXT_NODE) { + container.lastChild.textContent = container.lastChild.textContent.trimEnd(); + } + } + + // Create number cell and prepend it to the row + const nrEl = document.createElement('div'); + nrEl.className = 'item-nr'; + nrEl.textContent = nr; + + info.className = 'item-info'; + prices.className = 'item-prices'; + + row.prepend(nrEl); + }); +} From 45ce362c698f402780c74b64fe2ff8adf2f3c94b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 6 Feb 2026 19:08:11 +0100 Subject: [PATCH 2/4] Match original speisen grid layout with 4-area grid JS now creates 4 direct child divs (nr | info | desc | prices) instead of nesting desc inside info. CSS uses grid-template-areas matching the original 2-row layout where nr and prices span both rows. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ben Peter --- blocks/speisen/speisen.css | 20 ++++++++++++++++---- blocks/speisen/speisen.js | 23 +++++++++++++---------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/blocks/speisen/speisen.css b/blocks/speisen/speisen.css index 4611423..dcfc22c 100644 --- a/blocks/speisen/speisen.css +++ b/blocks/speisen/speisen.css @@ -1,17 +1,29 @@ -/* Speisen (menu) block — 3-column grid: nr | name+desc | prices */ +/* Speisen (menu) block — matches original 4-area grid layout */ .speisen > div { display: grid; - grid-template-columns: 2em 1fr auto; + grid-template: + "nr name prices" + "nr desc prices" / 2em auto auto; + gap: 0; margin-bottom: 8px; - align-items: baseline; } -.speisen .item-info .item-desc { +.speisen .item-nr { + grid-area: nr; +} + +.speisen .item-info { + grid-area: name; +} + +.speisen .item-desc { + grid-area: desc; font-size: smaller; font-style: italic; } .speisen .item-prices { + grid-area: prices; text-align: right; padding-left: 8px; } diff --git a/blocks/speisen/speisen.js b/blocks/speisen/speisen.js index 61109b2..159e30b 100644 --- a/blocks/speisen/speisen.js +++ b/blocks/speisen/speisen.js @@ -2,11 +2,12 @@ * Decorates the speisen (menu) block. * * Transforms a 2-column authored structure (item info | prices) - * into a 3-column visual layout (nr | name+desc | prices). + * into a 4-cell visual layout matching the original grid: + * .item-nr | .item-info | .item-desc | .item-prices * * Content model (Collection): * Col 1: **Nr** Item Name *optional description* - * Col 2: Price — or multi-line for size variants (e.g. "klein 3,30 €\ngroß 4,40 €") + * Col 2: Price — or multi-line for size variants */ export default function decorate(block) { [...block.children].forEach((row) => { @@ -26,7 +27,6 @@ export default function decorate(block) { if (/^[A-Za-z]?\d+[a-z]?$/.test(text)) { nr = text; firstEl.remove(); - // Remove leading whitespace left after the number const next = container.firstChild; if (next?.nodeType === Node.TEXT_NODE) { next.textContent = next.textContent.replace(/^\s+/, ''); @@ -34,29 +34,32 @@ export default function decorate(block) { } } - // Move description into its own paragraph below the name + // Extract description into a separate grid item (sibling, not child) + let descText = ''; const em = container.querySelector('em'); if (em) { - const descP = document.createElement('p'); - descP.className = 'item-desc'; - descP.textContent = em.textContent; - container.after(descP); + descText = em.textContent; em.remove(); - // Trim trailing whitespace from the name paragraph container.normalize(); if (container.lastChild?.nodeType === Node.TEXT_NODE) { container.lastChild.textContent = container.lastChild.textContent.trimEnd(); } } - // Create number cell and prepend it to the row + // Build the 4-cell row: nr | info | desc | prices const nrEl = document.createElement('div'); nrEl.className = 'item-nr'; nrEl.textContent = nr; + const descEl = document.createElement('div'); + descEl.className = 'item-desc'; + descEl.textContent = descText; + info.className = 'item-info'; prices.className = 'item-prices'; row.prepend(nrEl); + // Insert desc after info, before prices + prices.before(descEl); }); } From bb49718fe06859b882edb191aaa637b3af6e2fa8 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 6 Feb 2026 20:45:37 +0100 Subject: [PATCH 3/4] Add sizes/prices column split and pizza header row support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The speisen block now creates 5 grid cells per row (nr | info | desc | sizes | prices), matching the original 4-area grid layout. Prices with "label price" patterns (e.g. "0,3l 3,30 €") are automatically split into separate sizes and prices columns. Header rows with bold labels are detected and rendered as column headers. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ben Peter --- blocks/speisen/speisen.css | 12 +++-- blocks/speisen/speisen.js | 101 +++++++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/blocks/speisen/speisen.css b/blocks/speisen/speisen.css index dcfc22c..3460fae 100644 --- a/blocks/speisen/speisen.css +++ b/blocks/speisen/speisen.css @@ -1,9 +1,9 @@ -/* Speisen (menu) block — matches original 4-area grid layout */ +/* Speisen (menu) block — matches original 5-area grid layout */ .speisen > div { display: grid; grid-template: - "nr name prices" - "nr desc prices" / 2em auto auto; + "nr name sizes prices" + "nr desc sizes prices" / 2em auto auto 4em; gap: 0; margin-bottom: 8px; } @@ -20,6 +20,12 @@ grid-area: desc; font-size: smaller; font-style: italic; + text-align: left; +} + +.speisen .item-sizes { + grid-area: sizes; + text-align: right; } .speisen .item-prices { diff --git a/blocks/speisen/speisen.js b/blocks/speisen/speisen.js index 159e30b..89b10c9 100644 --- a/blocks/speisen/speisen.js +++ b/blocks/speisen/speisen.js @@ -2,19 +2,99 @@ * Decorates the speisen (menu) block. * * Transforms a 2-column authored structure (item info | prices) - * into a 4-cell visual layout matching the original grid: - * .item-nr | .item-info | .item-desc | .item-prices + * into a 5-cell visual layout matching the original grid: + * .item-nr | .item-info | .item-desc | .item-sizes | .item-prices * * Content model (Collection): * Col 1: **Nr** Item Name *optional description* - * Col 2: Price — or multi-line for size variants + * Col 2: Price — or multi-line for size variants (e.g. "0,3l 3,30 €") + * + * Multi-price handling: + * - "label price" patterns (e.g. "0,3l 3,30 €") split into sizes + prices columns + * - Bare multi-prices (e.g. pizza "7,00 €" / "8,00 €") distribute across columns + * - Header rows (empty info + bold labels) become column headers */ + +const PRICE_RE = /^(.+?)\s+(\d[\d,.]*\s*€)$/; + +function decorateHeaderRow(row, priceCol) { + const pElements = [...priceCol.querySelectorAll('p')]; + + const nrEl = document.createElement('div'); + nrEl.className = 'item-nr'; + + const infoEl = document.createElement('div'); + infoEl.className = 'item-info'; + + const descEl = document.createElement('div'); + descEl.className = 'item-desc'; + + const sizesEl = document.createElement('div'); + sizesEl.className = 'item-sizes'; + + const pricesEl = document.createElement('div'); + pricesEl.className = 'item-prices'; + + if (pElements.length >= 2) { + sizesEl.appendChild(pElements[0]); + pricesEl.appendChild(pElements[1]); + } else if (pElements.length === 1) { + pricesEl.appendChild(pElements[0]); + } + + row.classList.add('price-header'); + row.replaceChildren(nrEl, infoEl, descEl, sizesEl, pricesEl); +} + +function splitPrices(priceCol) { + const sizesEl = document.createElement('div'); + sizesEl.className = 'item-sizes'; + + const pricesEl = document.createElement('div'); + pricesEl.className = 'item-prices'; + + const pElements = [...priceCol.querySelectorAll('p')]; + + if (pElements.length === 0) { + // Raw text — move everything to prices + while (priceCol.firstChild) { + pricesEl.appendChild(priceCol.firstChild); + } + return { sizesEl, pricesEl }; + } + + const matches = pElements.map((p) => p.textContent.trim().match(PRICE_RE)); + + if (matches.every((m) => m !== null)) { + // All paragraphs have "label price" pattern → split + sizesEl.innerHTML = matches.map((m) => `

${m[1]}

`).join(''); + pricesEl.innerHTML = matches.map((m) => `

${m[2]}

`).join(''); + } else if (pElements.length > 1) { + // Multiple bare prices → first to sizes col, remaining to prices col + sizesEl.appendChild(pElements[0]); + for (let i = 1; i < pElements.length; i += 1) { + pricesEl.appendChild(pElements[i]); + } + } else { + // Single bare price → prices column only + pricesEl.appendChild(pElements[0]); + } + + return { sizesEl, pricesEl }; +} + export default function decorate(block) { [...block.children].forEach((row) => { const cols = [...row.children]; if (cols.length < 2) return; - const [info, prices] = cols; + const [info, priceCol] = cols; + + // Detect header row: empty info column with bold text in price column + if (!info.textContent.trim() && priceCol.querySelector('strong')) { + decorateHeaderRow(row, priceCol); + return; + } // Extract item number from a leading that matches Nr pattern const firstP = info.querySelector('p'); @@ -34,7 +114,7 @@ export default function decorate(block) { } } - // Extract description into a separate grid item (sibling, not child) + // Extract description into a separate grid item let descText = ''; const em = container.querySelector('em'); if (em) { @@ -46,7 +126,10 @@ export default function decorate(block) { } } - // Build the 4-cell row: nr | info | desc | prices + // Split prices into sizes + prices columns + const { sizesEl, pricesEl } = splitPrices(priceCol); + + // Build the 5-cell row: nr | info | desc | sizes | prices const nrEl = document.createElement('div'); nrEl.className = 'item-nr'; nrEl.textContent = nr; @@ -56,10 +139,6 @@ export default function decorate(block) { descEl.textContent = descText; info.className = 'item-info'; - prices.className = 'item-prices'; - - row.prepend(nrEl); - // Insert desc after info, before prices - prices.before(descEl); + row.replaceChildren(nrEl, info, descEl, sizesEl, pricesEl); }); } From e3da3224d56de7efbd0ed03445df98a8ff66e2fb Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 6 Feb 2026 20:59:10 +0100 Subject: [PATCH 4/4] Split header row bold label and measurement onto separate lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the original layout where "normal" and "Ø 28cm" are on separate lines in the sizes column, and "groß" and "Ø 30cm" on separate lines in the prices column. Prevents wrapping in the narrow 4em prices column. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ben Peter --- blocks/speisen/speisen.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/blocks/speisen/speisen.js b/blocks/speisen/speisen.js index 89b10c9..de51b10 100644 --- a/blocks/speisen/speisen.js +++ b/blocks/speisen/speisen.js @@ -17,6 +17,16 @@ const PRICE_RE = /^(.+?)\s+(\d[\d,.]*\s*€)$/; +function headerCellHTML(p) { + const strong = p.querySelector('strong'); + if (!strong) return p.outerHTML; + const boldText = strong.textContent; + const rest = p.textContent.replace(boldText, '').trim(); + let html = `

${boldText}

`; + if (rest) html += `

${rest}

`; + return html; +} + function decorateHeaderRow(row, priceCol) { const pElements = [...priceCol.querySelectorAll('p')]; @@ -36,10 +46,10 @@ function decorateHeaderRow(row, priceCol) { pricesEl.className = 'item-prices'; if (pElements.length >= 2) { - sizesEl.appendChild(pElements[0]); - pricesEl.appendChild(pElements[1]); + sizesEl.innerHTML = headerCellHTML(pElements[0]); + pricesEl.innerHTML = headerCellHTML(pElements[1]); } else if (pElements.length === 1) { - pricesEl.appendChild(pElements[0]); + pricesEl.innerHTML = headerCellHTML(pElements[0]); } row.classList.add('price-header');