From dc1f0da03e03820a679c93bf59d9e7b837668650 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Mon, 13 Oct 2025 16:05:02 -0700 Subject: [PATCH 1/4] Add recipe-nav block for previous/next recipe links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a new recipe-nav block that displays navigation links to the previous and next recipes. This block can be directly authored or populated from page metadata. Features: - Displays previous and next recipe links in a horizontal layout - Includes arrow icons for better UX - Fully responsive design (stacks vertically on mobile) - Accessible with proper ARIA labels and semantic HTML - Uses modern CSS with range notation for media queries - Follows project coding standards and passes all linting checks The block decorates authored content into a semantic nav structure with proper styling and hover states. Closes #6 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- blocks/recipe-nav/recipe-nav.css | 81 ++++++++++++++++++++++++++++++++ blocks/recipe-nav/recipe-nav.js | 76 ++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 blocks/recipe-nav/recipe-nav.css create mode 100644 blocks/recipe-nav/recipe-nav.js diff --git a/blocks/recipe-nav/recipe-nav.css b/blocks/recipe-nav/recipe-nav.css new file mode 100644 index 0000000..44c91d6 --- /dev/null +++ b/blocks/recipe-nav/recipe-nav.css @@ -0,0 +1,81 @@ +@layer blocks { + .recipe-nav { + background-color: var(--background-color); + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + } + + .recipe-nav-container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: var(--max-content-width); + margin: 0 auto; + padding: 16px; + gap: 16px; + } + + .recipe-nav-prev, + .recipe-nav-next { + flex: 0 1 auto; + } + + .recipe-nav-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + font-size: 14px; + font-weight: 600; + text-decoration: none; + color: var(--text-color); + background-color: transparent; + border: 1px solid #ccc; + border-radius: 4px; + transition: background-color 0.2s ease, border-color 0.2s ease; + } + + .recipe-nav-link:hover { + background-color: #f5f5f5; + border-color: #999; + } + + .recipe-nav-link:focus { + outline: 2px solid #005fcc; + outline-offset: 2px; + } + + .recipe-nav-icon { + font-size: 16px; + line-height: 1; + } + + /* Responsive adjustments */ + @media (width < 600px) { + .recipe-nav-container { + flex-direction: column; + gap: 12px; + } + + .recipe-nav-prev, + .recipe-nav-next { + width: 100%; + } + + .recipe-nav-link { + width: 100%; + justify-content: center; + } + } + + @media (width >= 900px) { + .recipe-nav-container { + padding: 20px 16px; + } + + .recipe-nav-link { + font-size: 16px; + padding: 10px 20px; + } + } +} diff --git a/blocks/recipe-nav/recipe-nav.js b/blocks/recipe-nav/recipe-nav.js new file mode 100644 index 0000000..1bafb14 --- /dev/null +++ b/blocks/recipe-nav/recipe-nav.js @@ -0,0 +1,76 @@ +/** + * Recipe navigation block + * Displays previous and next recipe links + * @param {Element} block The recipe-nav block element + */ +export default function decorate(block) { + // The block structure from authoring is: + //
+ //
+ //
Prev Recipe
+ //
Next Recipe
+ //
+ //
+ + // Transform into a more semantic structure + // Get the first row (direct child div) + const row = block.querySelector(':scope > div'); + if (!row) return; + + const cells = [...row.children]; + if (cells.length === 0) return; + + // Create navigation container + const nav = document.createElement('nav'); + nav.className = 'recipe-nav-container'; + nav.setAttribute('aria-label', 'Recipe navigation'); + + // Process prev link (first cell) + if (cells[0]) { + const prevLink = cells[0].querySelector('a'); + if (prevLink) { + const prevDiv = document.createElement('div'); + prevDiv.className = 'recipe-nav-prev'; + + // Add aria label for accessibility + prevLink.setAttribute('aria-label', 'Previous recipe'); + prevLink.className = 'recipe-nav-link'; + + // Add prev icon/arrow + const prevIcon = document.createElement('span'); + prevIcon.className = 'recipe-nav-icon'; + prevIcon.setAttribute('aria-hidden', 'true'); + prevIcon.textContent = '←'; + + prevLink.prepend(prevIcon); + prevDiv.append(prevLink); + nav.append(prevDiv); + } + } + + // Process next link (second cell) + if (cells[1]) { + const nextLink = cells[1].querySelector('a'); + if (nextLink) { + const nextDiv = document.createElement('div'); + nextDiv.className = 'recipe-nav-next'; + + // Add aria label for accessibility + nextLink.setAttribute('aria-label', 'Next recipe'); + nextLink.className = 'recipe-nav-link'; + + // Add next icon/arrow + const nextIcon = document.createElement('span'); + nextIcon.className = 'recipe-nav-icon'; + nextIcon.setAttribute('aria-hidden', 'true'); + nextIcon.textContent = '→'; + + nextLink.append(nextIcon); + nextDiv.append(nextLink); + nav.append(nextDiv); + } + } + + // Replace block content with navigation + block.replaceChildren(nav); +} From 5ab710c35d259ad01bc72356e964e82dfb69551e Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Mon, 13 Oct 2025 16:10:10 -0700 Subject: [PATCH 2/4] Update recipe-nav styling to match design reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove button-style borders and backgrounds for cleaner look - Use teal chevrons (‹ ›) instead of arrows - Style text as uppercase gray with proper letter-spacing - Increase spacing and adjust colors to match mockup - Maintain responsive behavior and accessibility features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- blocks/recipe-nav/recipe-nav.css | 29 ++++++++++++++--------------- blocks/recipe-nav/recipe-nav.js | 8 ++++---- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/blocks/recipe-nav/recipe-nav.css b/blocks/recipe-nav/recipe-nav.css index 44c91d6..20ecbba 100644 --- a/blocks/recipe-nav/recipe-nav.css +++ b/blocks/recipe-nav/recipe-nav.css @@ -1,8 +1,8 @@ @layer blocks { .recipe-nav { background-color: var(--background-color); - border-top: 1px solid #e0e0e0; - border-bottom: 1px solid #e0e0e0; + border-top: 1px solid #e5e5e5; + border-bottom: 1px solid #e5e5e5; } .recipe-nav-container { @@ -11,7 +11,7 @@ align-items: center; max-width: var(--max-content-width); margin: 0 auto; - padding: 16px; + padding: 20px 16px; gap: 16px; } @@ -23,21 +23,18 @@ .recipe-nav-link { display: inline-flex; align-items: center; - gap: 8px; - padding: 8px 16px; - font-size: 14px; - font-weight: 600; + gap: 10px; + font-size: 16px; + font-weight: 700; text-decoration: none; - color: var(--text-color); - background-color: transparent; - border: 1px solid #ccc; - border-radius: 4px; - transition: background-color 0.2s ease, border-color 0.2s ease; + color: #737373; + text-transform: uppercase; + letter-spacing: 0.5px; + transition: color 0.2s ease; } .recipe-nav-link:hover { - background-color: #f5f5f5; - border-color: #999; + color: #4a4a4a; } .recipe-nav-link:focus { @@ -46,8 +43,10 @@ } .recipe-nav-icon { - font-size: 16px; + font-size: 24px; line-height: 1; + color: #2a9d8f; + font-weight: 400; } /* Responsive adjustments */ diff --git a/blocks/recipe-nav/recipe-nav.js b/blocks/recipe-nav/recipe-nav.js index 1bafb14..2a88e2d 100644 --- a/blocks/recipe-nav/recipe-nav.js +++ b/blocks/recipe-nav/recipe-nav.js @@ -36,11 +36,11 @@ export default function decorate(block) { prevLink.setAttribute('aria-label', 'Previous recipe'); prevLink.className = 'recipe-nav-link'; - // Add prev icon/arrow + // Add prev icon/chevron const prevIcon = document.createElement('span'); prevIcon.className = 'recipe-nav-icon'; prevIcon.setAttribute('aria-hidden', 'true'); - prevIcon.textContent = '←'; + prevIcon.textContent = '‹'; prevLink.prepend(prevIcon); prevDiv.append(prevLink); @@ -59,11 +59,11 @@ export default function decorate(block) { nextLink.setAttribute('aria-label', 'Next recipe'); nextLink.className = 'recipe-nav-link'; - // Add next icon/arrow + // Add next icon/chevron const nextIcon = document.createElement('span'); nextIcon.className = 'recipe-nav-icon'; nextIcon.setAttribute('aria-hidden', 'true'); - nextIcon.textContent = '→'; + nextIcon.textContent = '›'; nextLink.append(nextIcon); nextDiv.append(nextLink); From ff5963d5d55961d17de0f4e0cf38fdb9c29f7d56 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Mon, 13 Oct 2025 16:23:43 -0700 Subject: [PATCH 3/4] Add recipe-info block for displaying recipe metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a responsive recipe information block that displays: - Level (difficulty) - Total time - Prep/Active time - Cook time (if present) - Yield (servings) - Nutrition Info link (non-functional per requirements) Layout: - Mobile: 2-column grid with specific item ordering - Desktop: 3-column grid with vertical dividers Resolves #9 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- blocks/recipe-info/recipe-info.css | 140 +++++++++++++++++++++++++++++ blocks/recipe-info/recipe-info.js | 60 +++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 blocks/recipe-info/recipe-info.css create mode 100644 blocks/recipe-info/recipe-info.js diff --git a/blocks/recipe-info/recipe-info.css b/blocks/recipe-info/recipe-info.css new file mode 100644 index 0000000..f60c750 --- /dev/null +++ b/blocks/recipe-info/recipe-info.css @@ -0,0 +1,140 @@ +.recipe-info { + width: 100%; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 20px 16px; + margin: 20px 0; +} + +.recipe-info-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px 32px; + max-width: 1200px; + margin: 0 auto; +} + +.recipe-info-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.recipe-info-label { + font-size: 14px; + line-height: 1.4; + color: #333; + font-weight: normal; +} + +.recipe-info-value { + font-size: 16px; + line-height: 1.4; + font-weight: bold; + color: #000; +} + +/* Mobile layout: + Column 1: Level, Yield, Nutrition Info + Column 2: Total, Prep/Active, Cook (if present) +*/ +.recipe-info-item:nth-child(1) { + grid-column: 1; + order: 1; +} + +.recipe-info-item:nth-child(2) { + grid-column: 2; + order: 1; +} + +.recipe-info-item:nth-child(3) { + grid-column: 2; + order: 2; +} + +.recipe-info-item:nth-child(4) { + grid-column: 1; + order: 2; +} + +.recipe-info-item.nutrition-info { + grid-column: 1; + order: 3; +} + +.nutrition-link { + color: #d73400; + text-decoration: none; + font-size: 16px; + font-weight: 600; + cursor: pointer; +} + +.nutrition-link:hover { + text-decoration: underline; +} + +/* Tablet and desktop - 3 column layout */ +@media (width >= 600px) { + .recipe-info { + padding: 24px 0; + } + + .recipe-info-container { + grid-template-columns: 1fr 1fr 1fr; + gap: 0; + } + + .recipe-info-item { + padding: 0 32px; + border-right: 1px solid #ddd; + order: initial; + } + + .recipe-info-item:last-child { + border-right: none; + } + + /* Desktop layout: + Column 1: Level, Total + Column 2: Prep/Active, Cook (if present) + Column 3: Yield, Nutrition Info + */ + .recipe-info-item:nth-child(1) { + grid-column: 1; + grid-row: 1; + } + + .recipe-info-item:nth-child(2) { + grid-column: 1; + grid-row: 2; + } + + .recipe-info-item:nth-child(3) { + grid-column: 2; + grid-row: 1; + } + + .recipe-info-item:nth-child(4) { + grid-column: 3; + grid-row: 1; + } + + .recipe-info-item.nutrition-info { + grid-column: 3; + grid-row: 2; + } + + /* If there's a 5th item (Cook time), adjust layout */ + .recipe-info-item:nth-child(5) { + grid-column: 2; + grid-row: 2; + } + + /* Adjust nutrition info when there are 5 items */ + .recipe-info-item:nth-child(5) ~ .nutrition-info { + grid-column: 3; + grid-row: 2; + } +} diff --git a/blocks/recipe-info/recipe-info.js b/blocks/recipe-info/recipe-info.js new file mode 100644 index 0000000..4d30705 --- /dev/null +++ b/blocks/recipe-info/recipe-info.js @@ -0,0 +1,60 @@ +/** + * loads and decorates the recipe-info block + * @param {Element} block The recipe-info element + */ +export default async function decorate(block) { + // Extract all info items from the block + const items = Array.from(block.children).map((row) => { + const cells = Array.from(row.children); + if (cells.length === 2) { + return { + label: cells[0].textContent.trim(), + value: cells[1].textContent.trim(), + }; + } + return null; + }).filter((item) => item !== null); + + // Clear the block + block.innerHTML = ''; + + // Create a container for the info items + const container = document.createElement('div'); + container.className = 'recipe-info-container'; + + // Create info items based on what's available + items.forEach((item) => { + const infoItem = document.createElement('div'); + infoItem.className = 'recipe-info-item'; + + const label = document.createElement('div'); + label.className = 'recipe-info-label'; + label.textContent = `${item.label}:`; + + const value = document.createElement('div'); + value.className = 'recipe-info-value'; + value.textContent = item.value; + + infoItem.appendChild(label); + infoItem.appendChild(value); + container.appendChild(infoItem); + }); + + // Add nutrition info link + const nutritionItem = document.createElement('div'); + nutritionItem.className = 'recipe-info-item nutrition-info'; + + const nutritionLink = document.createElement('a'); + nutritionLink.href = '#'; + nutritionLink.className = 'nutrition-link'; + nutritionLink.textContent = 'Nutrition Info'; + nutritionLink.addEventListener('click', (e) => { + e.preventDefault(); + // For now, the link doesn't do anything as per requirements + }); + + nutritionItem.appendChild(nutritionLink); + container.appendChild(nutritionItem); + + block.appendChild(container); +} From 62191e47bd1b87124448465f0b2a4be74d1ee21b Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Tue, 14 Oct 2025 14:26:22 -0700 Subject: [PATCH 4/4] Add editorial-promo block for recipe sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a card-style promo block for displaying editorial content links with thumbnail images. Each item shows an image thumbnail (161x121px) alongside linked text. Features: - Vertical list layout with 16px gap between items - Optimized images using createOptimizedPicture - Hover effects on links - Responsive design - Clean semantic HTML structure Closes #18 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- blocks/editorial-promo/editorial-promo.css | 53 ++++++++++++++++++++++ blocks/editorial-promo/editorial-promo.js | 51 +++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 blocks/editorial-promo/editorial-promo.css create mode 100644 blocks/editorial-promo/editorial-promo.js diff --git a/blocks/editorial-promo/editorial-promo.css b/blocks/editorial-promo/editorial-promo.css new file mode 100644 index 0000000..f09f949 --- /dev/null +++ b/blocks/editorial-promo/editorial-promo.css @@ -0,0 +1,53 @@ +.editorial-promo ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 16px; +} + +.editorial-promo li { + margin: 0; + padding: 0; +} + +.editorial-promo-link { + display: flex; + gap: 16px; + text-decoration: none; + color: inherit; + transition: opacity 0.2s ease; +} + +.editorial-promo-link:hover { + opacity: 0.8; +} + +.editorial-promo-image { + flex-shrink: 0; + width: 161px; + height: 121px; + overflow: hidden; + line-height: 0; +} + +.editorial-promo-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.editorial-promo-text { + flex: 1; + display: flex; + align-items: center; + font-weight: 600; + font-size: 16px; + line-height: 1.3; + color: #000; +} + +.editorial-promo-link:hover .editorial-promo-text { + text-decoration: underline; +} diff --git a/blocks/editorial-promo/editorial-promo.js b/blocks/editorial-promo/editorial-promo.js new file mode 100644 index 0000000..8e453f1 --- /dev/null +++ b/blocks/editorial-promo/editorial-promo.js @@ -0,0 +1,51 @@ +import { createOptimizedPicture } from '../../scripts/aem.js'; + +/** + * loads and decorates the editorial promo block + * @param {Element} block The block element + */ +export default function decorate(block) { + // Convert block to unordered list + const ul = document.createElement('ul'); + [...block.children].forEach((row) => { + const li = document.createElement('li'); + + // Extract image and link from row + const imageDiv = row.children[0]; + const linkDiv = row.children[1]; + + if (imageDiv && linkDiv) { + // Create link wrapper + const link = linkDiv.querySelector('a'); + if (link) { + const anchor = document.createElement('a'); + anchor.href = link.href; + anchor.className = 'editorial-promo-link'; + + // Add image container + const imageContainer = document.createElement('div'); + imageContainer.className = 'editorial-promo-image'; + const picture = imageDiv.querySelector('picture'); + if (picture) { + const img = picture.querySelector('img'); + if (img) { + imageContainer.append(createOptimizedPicture(img.src, img.alt, false, [{ width: '161' }])); + } + } + + // Add text container + const textContainer = document.createElement('div'); + textContainer.className = 'editorial-promo-text'; + textContainer.textContent = link.textContent; + + anchor.append(imageContainer); + anchor.append(textContainer); + li.append(anchor); + } + } + + ul.append(li); + }); + + block.replaceChildren(ul); +}