diff --git a/blocks/carousel/_carousel.json b/blocks/carousel/_carousel.json new file mode 100644 index 0000000..db15056 --- /dev/null +++ b/blocks/carousel/_carousel.json @@ -0,0 +1,104 @@ +{ + "definitions": [ + { + "title": "Carousel", + "id": "carousel", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Carousel", + "filter": "carousel" + } + } + } + } + }, + { + "title": "Carousel Card", + "id": "carousel-cards", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block/item", + "template": { + "name": "Carousel Cards", + "model": "carousel-cards" + } + } + } + } + } + ], + "models": [ + { + "id": "carousel", + "fields": [] + }, + { + "id": "carousel-cards", + "fields": [ + { + "component": "reference", + "valueType": "string", + "name": "image", + "label": "Image" + }, + { + "component": "select", + "valueType": "string", + "name": "alignment", + "label": "Card Alignment", + "value": "center", + "options": [ + { + "name": "Left", + "value": "left" + }, + { + "name": "Center", + "value": "center" + }, + { + "name": "Right", + "value": "right" + } + ] + }, + { + "component": "text", + "valueType": "string", + "name": "title", + "label": "Card Title" + }, + { + "component": "richtext", + "valueType": "string", + "name": "description", + "label": "Card Description" + }, + { + "component": "text", + "valueType": "string", + "name": "buttonLabel", + "label": "Button Label" + }, + { + "component": "aem-content", + "name": "buttonLink", + "label": "Button Link" + } + ] + } + ], + "filters": [ + { + "id": "carousel", + "components": [ + "carousel-cards" + ] + } + ] +} + diff --git a/blocks/carousel/carousel.css b/blocks/carousel/carousel.css new file mode 100644 index 0000000..73af487 --- /dev/null +++ b/blocks/carousel/carousel.css @@ -0,0 +1,261 @@ +/* ========================= + Base Carousel Layout +========================= */ + +.carousel { + position: relative; + overflow: hidden; +} + +.carousel-viewport { + overflow: hidden; + width: 100%; +} + +.carousel-track { + display: flex; + transition: transform 0.6s ease; +} + +.carousel-slide { + min-width: 100%; + position: relative; +} + +/* Image */ +.carousel-slide picture img { + width: 100%; + height: 500px; + object-fit: cover; + display: block; +} + +/* ========================= + Overlay Base (Auto Height) +========================= */ + +.carousel-overlay { + position: absolute; + bottom: 40px; + padding: 2.5rem 3rem; + width: 45%; + color: #fff; + z-index: 2; + border-radius: 6px; + background: rgba(0, 0, 0, 0.65); +} + +/* Gradient background for readability */ +.carousel-overlay::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient( + to right, + rgba(0, 0, 0, 0.75), + rgba(0, 0, 0, 0.4), + transparent + ); +} + +/* ========================= + Alignment Variants +========================= */ + +/* LEFT */ +.overlay-left { + left: 60px; + text-align: left; +} + +/* RIGHT */ +.overlay-right { + right: 60px; + text-align: left; +} + +/* CENTER */ +.overlay-center { + left: 50%; + transform: translateX(-50%); + text-align: center; + width: 60%; +} + +.overlay-right::before { + background: linear-gradient( + to left, + rgba(0, 0, 0, 0.75), + rgba(0, 0, 0, 0.4), + transparent + ); +} + +.overlay-center::before { + background: rgba(0, 0, 0, -0.45); +} + +/* ========================= + Overlay cells (preserve UE bindings) +========================= */ + +/* Hidden cells: keep in DOM for UE, hide visually */ +.carousel-overlay .carousel-alignment, +.carousel-overlay .carousel-button-style, +.carousel-overlay .carousel-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.carousel-overlay .carousel-title { + font-size: 2.5rem; + margin-bottom: 1rem; + line-height: 1.2; + font-weight: 700; +} + +.carousel-overlay .carousel-desc { + font-size: 1.1rem; + margin: 0.5rem 0; +} + +/* ========================= + Typography +========================= */ + +.carousel-overlay h1, +.carousel-overlay h2, +.carousel-overlay h3 { + font-size: 2.5rem; + margin-bottom: 1rem; + line-height: 1.2; +} + +.carousel-overlay p { + font-size: 1.1rem; + margin: 0.5rem 0; +} + +/* ========================= + Button Styling +========================= */ + +/* Higher specificity than styles.css a.button:any-link (0,2,1) */ +.carousel .carousel-overlay .carousel-button { + display: inline-block; + margin-top: 1.8rem; + padding: 0.85rem 2rem; + background-color: #C7C6C1; + color: #000 !important; + text-decoration: none !important; + font-weight: 600; + border-radius: 4px; + transition: all 0.3s ease; +} + +.carousel .carousel-overlay .carousel-button:hover { + background-color: #000; + color: #fff !important; + text-decoration: none !important; +} + +.carousel-overlay a { + text-decoration: none; +} + +/* ========================= + Navigation Arrows +========================= */ + +.carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.6); + border: none; + color: #fff; + padding: 1rem 1.2rem; + cursor: pointer; + z-index: 3; + font-size: 1.2rem; + border-radius: 4px; + transition: 0.3s ease; +} + +.carousel-nav.prev { + left: 20px; +} + +.carousel-nav.next { + right: 20px; +} + +.carousel-nav:hover { + background: rgba(0, 0, 0, 0.9); +} + +/* ========================= + Responsive +========================= */ + +@media (max-width: 992px) { + .carousel-slide picture img { + height: 450px; + } + + .carousel-overlay { + width: 70%; + padding: 3rem 2rem; + } + + .carousel-overlay h1, + .carousel-overlay h2, + .carousel-overlay h3 { + font-size: 2.2rem; + } +} + +@media (max-width: 768px) { + .carousel-slide picture img { + height: 380px; + } + + .carousel-overlay { + width: 90%; + left: 50% !important; + right: auto !important; + transform: translateX(-50%) !important; + bottom: 20px; + text-align: center; + } + + .overlay-left, + .overlay-right, + .overlay-center { + left: 0; + right: 0; + transform: none; + } + + .carousel-overlay::before { + background: rgba(0, 0, 0, 0.6); + } + + .carousel-overlay h1, + .carousel-overlay h2, + .carousel-overlay h3 { + font-size: 1.8rem; + } + + .carousel-overlay p { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/blocks/carousel/carousel.js b/blocks/carousel/carousel.js new file mode 100644 index 0000000..17d96d0 --- /dev/null +++ b/blocks/carousel/carousel.js @@ -0,0 +1,154 @@ +export default function decorate(block) { + const originalSlides = [...block.children]; + if (originalSlides.length <= 1) return; + + const viewport = document.createElement('div'); + viewport.className = 'carousel-viewport'; + + const track = document.createElement('div'); + track.className = 'carousel-track'; + + /* ---------------------------- + Prepare Slides (overlay logic) + ----------------------------- */ + + const getCellText = (el) => (el?.textContent?.trim() ?? ''); + const applyButtonClass = (linkCell, label) => { + const a = linkCell?.querySelector('a'); + if (a) { + a.classList.add('carousel-button'); + a.textContent = label || a.textContent || 'CTA Button'; + } + }; + + originalSlides.forEach((slide) => { + slide.classList.add('carousel-slide'); + + const cells = [...slide.children]; + const alignmentCell = cells[1]; + const titleCell = cells[2]; + const descCell = cells[3]; + const buttonLabelCell = cells[4]; + const buttonLinkCell = cells[5]; + const styleCell = cells[6]; + + const alignmentRaw = getCellText(alignmentCell).toLowerCase(); + let alignment = 'left'; + if (alignmentRaw === 'center') alignment = 'center'; + else if (alignmentRaw === 'right') alignment = 'right'; + + const title = getCellText(titleCell); + const description = getCellText(descCell); + const buttonLabel = getCellText(buttonLabelCell); + const hasLink = buttonLinkCell?.querySelector('a') || getCellText(buttonLinkCell); + const hasDetails = !!(title || description || buttonLabel || hasLink); + + if (hasDetails) { + const overlay = document.createElement('div'); + overlay.className = `carousel-overlay overlay-${alignment}`; + + [alignmentCell, titleCell, descCell, buttonLabelCell, buttonLinkCell, styleCell].forEach((c) => { + if (c?.parentNode) { + c.classList.add('carousel-overlay-cell'); + if (c === alignmentCell) c.classList.add('carousel-alignment'); + if (c === titleCell) c.classList.add('carousel-title'); + if (c === descCell) c.classList.add('carousel-desc'); + if (c === buttonLabelCell) c.classList.add('carousel-label'); + if (c === buttonLinkCell) { + c.classList.add('carousel-link'); + applyButtonClass(c, buttonLabel); + } + if (c === styleCell) c.classList.add('carousel-button-style'); + overlay.appendChild(c); + } + }); + slide.appendChild(overlay); + } else { + [alignmentCell, titleCell, descCell, buttonLabelCell, buttonLinkCell, styleCell].forEach((c) => { + if (c?.parentNode) c.remove(); + }); + } + }); + + /* ---------------------------- + Infinite Loop Setup + ----------------------------- */ + + const stripEditorAttrs = (el) => { + const nodes = [el, ...el.querySelectorAll('*')]; + nodes.forEach((node) => { + [...node.attributes].forEach((attr) => { + if (attr.name.startsWith('data-aue-') || attr.name.startsWith('data-richtext-')) { + node.removeAttribute(attr.name); + } + }); + }); + }; + + const firstClone = originalSlides[0].cloneNode(true); + const lastClone = originalSlides[originalSlides.length - 1].cloneNode(true); + + [firstClone, lastClone].forEach((clone) => { + clone.classList.add('clone'); + stripEditorAttrs(clone); + }); + + track.append(lastClone); + originalSlides.forEach((slide) => track.append(slide)); + track.append(firstClone); + + viewport.append(track); + block.textContent = ''; + block.append(viewport); + + /* ---------------------------- + Navigation + ----------------------------- */ + + const prevBtn = document.createElement('button'); + prevBtn.className = 'carousel-nav prev'; + prevBtn.innerHTML = '❮'; + + const nextBtn = document.createElement('button'); + nextBtn.className = 'carousel-nav next'; + nextBtn.innerHTML = '❯'; + + block.append(prevBtn, nextBtn); + + let currentIndex = 1; + const totalSlides = originalSlides.length; + + function setPosition(withTransition = true) { + track.style.transition = withTransition + ? 'transform 0.6s ease' + : 'none'; + + track.style.transform = `translateX(-${currentIndex * 100}%)`; + } + + setPosition(false); + + nextBtn.addEventListener('click', () => { + if (currentIndex >= totalSlides + 1) return; + currentIndex++; + setPosition(); + }); + + prevBtn.addEventListener('click', () => { + if (currentIndex <= 0) return; + currentIndex--; + setPosition(); + }); + + track.addEventListener('transitionend', () => { + if (currentIndex === totalSlides + 1) { + currentIndex = 1; + setPosition(false); + } + + if (currentIndex === 0) { + currentIndex = totalSlides; + setPosition(false); + } + }); +} \ No newline at end of file diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index f2b203e..f96f2b2 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -5,6 +5,7 @@ .columns img { width: 100%; + border-radius: 8px; } .columns > div > div { @@ -17,6 +18,7 @@ .columns > div > .columns-img-col img { display: block; + border-radius: 8px; } @media (width >= 900px) { @@ -31,3 +33,56 @@ order: unset; } } + +.columns a:any-link { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + max-width: 100%; + margin: 0; + border: 2px solid var(--text-color); + border-radius: 2.4em; + padding: 0.5em 1.2em; + font: inherit; + font-weight: 500; + line-height: 1.25; + text-decoration: none; + background-color: transparent; + color: var(--text-color); + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.columns a:hover, +.columns a:focus-visible { + border-color: var(--dark-color); + background-color: var(--light-color); + color: var(--dark-color); + text-decoration: none; +} + +footer .columns a:any-link, +footer .footer .columns a:any-link { + border: none; + border-radius: 0; + padding: 0; + margin: 0; + background-color: transparent; + font-weight: normal; + display: inline; + text-decoration: none; + color: var(--text-color); +} + +footer .columns a:hover, +footer .columns a:focus-visible, +footer .footer .columns a:hover, +footer .footer .columns a:focus-visible { + border: none; + background-color: transparent; + color: var(--text-color); + text-decoration: underline; +} diff --git a/blocks/contact-form/_contact-form.json b/blocks/contact-form/_contact-form.json new file mode 100644 index 0000000..2b426f0 --- /dev/null +++ b/blocks/contact-form/_contact-form.json @@ -0,0 +1,42 @@ +{ + "definitions": [ + { + "title": "Contact Form", + "id": "contact-form", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Contact Form", + "model": "contact-form" + } + } + } + } + } + ], + "models": [ + { + "id": "contact-form", + "fields": [ + { + "component": "text", + "valueType": "string", + "name": "endpoint", + "label": "Submission Endpoint URL", + "description": "HTTPS endpoint that receives JSON and writes to AEM or Google Sheet." + }, + { + "component": "text", + "valueType": "string", + "name": "successMessage", + "label": "Success Message", + "value": "Thank you. Your request has been submitted." + } + ] + } + ], + "filters": [] +} + diff --git a/blocks/contact-form/contact-form.css b/blocks/contact-form/contact-form.css new file mode 100644 index 0000000..06cddcb --- /dev/null +++ b/blocks/contact-form/contact-form.css @@ -0,0 +1,152 @@ +.contact-form-wrapper { + max-width: 640px; + margin: 0 auto; +} + +.contact-form { + padding: 24px 24px 28px; + border-radius: 16px; + background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 100%); + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.contact-form-title { + margin-top: 0; + margin-bottom: 4px; +} + +.contact-form-description { + margin-top: 0; + margin-bottom: 16px; + font-size: var(--body-font-size-s); + color: var(--dark-color); +} + +.contact-form-field { + margin-top: 12px; + display: flex; + flex-direction: column; +} + +.contact-form-field label { + margin-bottom: 4px; + font-size: var(--body-font-size-xs); + font-weight: 500; +} + +.contact-form-field input[type='text'], +.contact-form-field input[type='email'], +.contact-form-field input[type='tel'], +.contact-form-field textarea { + border-radius: 8px; + border: 1px solid #d0d7e2; + padding: 8px 10px; + font-size: var(--body-font-size-xs); + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.contact-form-field input[type='text']:focus, +.contact-form-field input[type='email']:focus, +.contact-form-field input[type='tel']:focus, +.contact-form-field textarea:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(59, 99, 251, 0.1); +} + +.contact-form-field textarea { + resize: vertical; + min-height: 96px; +} + +.contact-form-error { + min-height: 16px; + margin-top: 4px; + font-size: 12px; + color: #c62828; +} + +.contact-form-terms { + flex-direction: row; + align-items: flex-start; + gap: 8px; +} + +.contact-form-terms input[type='checkbox'] { + margin-top: 4px; +} + +.contact-form-terms label { + margin: 0; + font-size: var(--body-font-size-xs); +} + +.contact-form-terms-link { + border: 0; + padding: 0; + margin: 0; + background: none; + color: var(--link-color); + text-decoration: underline; + cursor: pointer; + font: inherit; +} + +.contact-form-status { + margin-top: 8px; + font-size: 13px; +} + +.contact-form-status.success { + color: #2e7d32; +} + +.contact-form-status.error { + color: #c62828; +} + +.contact-form-submit { + margin-top: 16px; +} + +.contact-form-modal-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.contact-form-modal { + max-width: 480px; + width: calc(100% - 32px); + background-color: #ffffff; + border-radius: 12px; + padding: 20px 20px 16px; + box-shadow: + 0 14px 45px rgba(0, 0, 0, 0.25), + 0 10px 18px rgba(0, 0, 0, 0.22); +} + +.contact-form-modal-body { + font-size: var(--body-font-size-xs); +} + +.contact-form-modal-body ul { + padding-left: 20px; +} + +.contact-form-modal-close { + margin-top: 12px; +} + +@media (width >= 900px) { + .contact-form { + padding: 28px 32px 32px; + } +} + diff --git a/blocks/contact-form/contact-form.js b/blocks/contact-form/contact-form.js new file mode 100644 index 0000000..a1a3901 --- /dev/null +++ b/blocks/contact-form/contact-form.js @@ -0,0 +1,286 @@ +function buildInput(labelText, id, type = 'text', required = false) { + const field = document.createElement('div'); + field.className = 'contact-form-field'; + + const label = document.createElement('label'); + label.setAttribute('for', id); + label.textContent = labelText; + + const input = document.createElement('input'); + input.id = id; + input.name = id; + input.type = type; + if (required) input.required = true; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + field.append(label, input, error); + return { field, input, error }; +} + +function buildTextArea(labelText, id, required = false) { + const field = document.createElement('div'); + field.className = 'contact-form-field'; + + const label = document.createElement('label'); + label.setAttribute('for', id); + label.textContent = labelText; + + const textarea = document.createElement('textarea'); + textarea.id = id; + textarea.name = id; + textarea.rows = 4; + if (required) textarea.required = true; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + field.append(label, textarea, error); + return { field, textarea, error }; +} + +function buildTermsField() { + const wrapper = document.createElement('div'); + wrapper.className = 'contact-form-field contact-form-terms'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = 'cf-terms'; + checkbox.name = 'cf-terms'; + checkbox.required = true; + + const label = document.createElement('label'); + label.setAttribute('for', 'cf-terms'); + label.innerHTML = 'I have read and agree to the .'; + + const error = document.createElement('div'); + error.className = 'contact-form-error'; + error.setAttribute('aria-live', 'polite'); + + wrapper.append(checkbox, label, error); + return { wrapper, checkbox, error }; +} + +function buildModal() { + const overlay = document.createElement('div'); + overlay.className = 'contact-form-modal-overlay'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-labelledby', 'cf-terms-title'); + + const dialog = document.createElement('div'); + dialog.className = 'contact-form-modal'; + + const title = document.createElement('h3'); + title.id = 'cf-terms-title'; + title.textContent = 'Terms and Conditions'; + + const body = document.createElement('div'); + body.className = 'contact-form-modal-body'; + body.innerHTML = ` +
Please review the trek terms and conditions before submitting the form.
+