diff --git a/creativecloud/blocks/firefly-carousel/firefly-carousel.css b/creativecloud/blocks/firefly-carousel/firefly-carousel.css new file mode 100644 index 000000000..60245e759 --- /dev/null +++ b/creativecloud/blocks/firefly-carousel/firefly-carousel.css @@ -0,0 +1,223 @@ +.firefly-carousel { + --ff-gap: 8px; + --ff-card-width: 80vw; + --ff-card-height: calc(var(--ff-card-width) / 1.4); + --ff-nav-size: 40px; + --ff-control-gap: 8px; + --ff-card-border-radius: 16px; + padding: 0; + position: relative; +} + +.firefly-carousel { + width: 100%; +} + +.firefly-carousel .firefly-carousel-viewport { + width: 100%; + overflow: hidden; + position: relative; + padding-bottom: 160px; +} + +.firefly-carousel .firefly-carousel-track { + display: flex; + gap: var(--ff-gap); + will-change: transform; + transition: transform 320ms ease; + margin-left: calc((100% - var(--ff-card-width)) / 2); +} + +.firefly-carousel .firefly-carousel-card { + position: relative; + flex: 0 0 var(--ff-card-width); + width: var(--ff-card-width); + height: var(--ff-card-height); + overflow: hidden; + border-radius: var(--ff-card-border-radius); + background: #111; + outline: none; +} + +.firefly-carousel .firefly-carousel-card:focus-visible::after { + content: ''; + position: absolute; + inset: 0; + border: 2px solid #1473E6; + border-radius: var(--ff-card-border-radius); + pointer-events: none; + z-index: 2; +} + +.firefly-carousel .firefly-carousel-card.active { + /* Slightly reduced shadow extent (5px) to avoid clipping by content below */ + box-shadow: 0 95px 75px 0 rgba(0, 0, 0, 0.19), 0 64.815px 46.852px 0 rgba(0, 0, 0, 0.14), 0 38.519px 25.481px 0 rgba(0, 0, 0, 0.12), 0 20px 13px 0 rgba(0, 0, 0, 0.09), 0 8.148px 6.519px 0 rgba(0, 0, 0, 0.07), 0 1.852px 3.148px 0 rgba(0, 0, 0, 0.05); +} + +.firefly-carousel .firefly-carousel-media { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.firefly-carousel .firefly-carousel-media picture, +.firefly-carousel .firefly-carousel-media img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.firefly-carousel .firefly-carousel-prompt { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + display: none; + flex-direction: row; + align-items: center; + padding: 8px 9px 8px 8px; + border-radius: 8px; + border: 1px solid var(--Color-Middle-Grey, #707070); + background: var(--container-fill, #FFF); + box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.25); + width: fit-content; + max-width: calc(var(--ff-card-width) - 80px); + color: inherit; + text-decoration: none; + cursor: pointer; + font-family: inherit; + text-align: start; +} + +.firefly-carousel .firefly-carousel-card.active .firefly-carousel-prompt { + display: flex; +} + +[dir="rtl"] .firefly-carousel .firefly-carousel-prompt { + left: 56px; + right: 16px; +} + +.firefly-carousel .firefly-carousel-prompt-text { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.14px; + color: inherit; + text-decoration: none; +} + +.firefly-carousel .firefly-carousel-prompt-icon { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: flex-end; + width: 16px; + margin-left: 24px; + height: 16px; +} + +.firefly-carousel .firefly-carousel-nav-btn { + width: var(--ff-nav-size); + height: var(--ff-nav-size); + display: inline-flex; + justify-content: center; + align-items: center; + position: absolute; + top: calc(var(--ff-card-height) / 2); + z-index: 1; + border-radius: 90.909px; + background: #010101; + backdrop-filter: blur(13.63636302947998px); + border: none; + cursor: pointer; +} + +.firefly-carousel .firefly-carousel-nav-btn:focus-visible::after { + content: ''; + position: absolute; + inset: 0; + border: 2px solid #1473E6; + border-radius: 90.909px; + pointer-events: none; + z-index: 2; +} + +.firefly-carousel .firefly-carousel-nav-btn.prev { + left: calc((100% - var(--ff-card-width)) / 2); + transform: translate(-50%, -50%); +} + +.firefly-carousel .firefly-carousel-nav-btn.next { + right: calc((100% - var(--ff-card-width)) / 2); + transform: translate(50%, -50%); +} + +.firefly-carousel .firefly-carousel-nav-btn:disabled { + opacity: 0.5; + background: rgba(80, 80, 80, 0.40); + backdrop-filter: blur(13.63636302947998px); +} + +.firefly-carousel .firefly-carousel-nav-btn svg { + width: 10.909px; + height: 10.909px; + flex-shrink: 0; + color: #FFFFFF; +} + +@media screen and (min-width: 600px) { + .firefly-carousel { + --ff-card-width: 70vw; + --ff-card-height: calc(var(--ff-card-width) / 1.6); + --ff-card-border-radius: 12px; + } + + .firefly-carousel .firefly-carousel-prompt { + left: 30px; + bottom: 20px; + transform: none; + max-width: calc(var(--ff-card-width) / 2.2); + } +} + +@media screen and (min-width: 1200px) { + .firefly-carousel { + --ff-card-width: 60vw; + --ff-card-height: calc(var(--ff-card-width) / 1.6); + --ff-card-border-radius: 24px; + --ff-nav-size: 65px; + } + + .firefly-carousel .firefly-carousel-card.active .firefly-carousel-prompt { + padding: 16px 18px 16px 16px; + border-radius: 11px; + left: 55px; + bottom: 55px; + max-width: calc(var(--ff-card-width) / 1.6); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 180ms ease; + } + + .firefly-carousel .firefly-carousel-card.active:hover .firefly-carousel-prompt, + .firefly-carousel .firefly-carousel-card.active:focus-within .firefly-carousel-prompt { + opacity: 1; + visibility: visible; + pointer-events: auto; + } + + .firefly-carousel .firefly-carousel-prompt-text { + font-size: 22px; + letter-spacing: -0.22px; + } +} diff --git a/creativecloud/blocks/firefly-carousel/firefly-carousel.js b/creativecloud/blocks/firefly-carousel/firefly-carousel.js new file mode 100644 index 000000000..3e25cc0c1 --- /dev/null +++ b/creativecloud/blocks/firefly-carousel/firefly-carousel.js @@ -0,0 +1,335 @@ +import { createTag } from '../../scripts/utils.js'; + +const BLOCK = 'firefly-carousel'; +const GAP = 8; +const TRANSITION_FALLBACK_MS = 420; +const AUTOSCROLL_INTERVAL_MS = 4000; +const NAV_DIRECTIONS = { + PREV: 'prev', + NEXT: 'next', +}; +const NAV_INDEX_DELTAS = { + [NAV_DIRECTIONS.PREV]: -1, + [NAV_DIRECTIONS.NEXT]: 1, +}; +const NAV_FRAMES = { + BASE: { beforeActive: 1, offsetMultiplier: -1 }, + NEXT: { beforeActive: 1, offsetMultiplier: -2 }, + PREV_STAGE: { beforeActive: 2, offsetMultiplier: -2 }, + PREV: { beforeActive: 2, offsetMultiplier: -1 }, +}; +const NAV_SEQUENCES = { + [NAV_DIRECTIONS.NEXT]: [ + { frame: NAV_FRAMES.NEXT, animate: true }, + ], + [NAV_DIRECTIONS.PREV]: [ + { frame: NAV_FRAMES.PREV_STAGE, animate: false }, + { frame: NAV_FRAMES.PREV, animate: true }, + ], +}; + +const ICONS = { + prompt: ` + + +`, + navPrev: ` + +`, + navNext: ` + +`, +}; + +// Parses carousel items directly from the block DOM. Each item has picture, caption, and deeplink +function parseItemsFromDOM(el) { + const itemDivs = el.querySelectorAll(':scope > div'); + const items = []; + + itemDivs.forEach((itemDiv) => { + const [mediaDiv, captionDiv, deeplinkDiv] = itemDiv.children; + const pictureEl = mediaDiv?.querySelector('picture'); + const deeplinkAnchor = deeplinkDiv?.querySelector('a[href]'); + if (!pictureEl) return; + + items.push({ + pictureEl, + promptText: captionDiv?.textContent?.trim() || '', + deeplinkUrl: deeplinkAnchor?.getAttribute('href') || '', + }); + }); + return items; +} + +function wrapIndex(index, length) { + return (index + length) % length; +} + +function createPromptPill(promptText, deeplinkUrl) { + const attrs = { + class: `${BLOCK}-prompt`, + title: promptText || '', + 'aria-label': promptText || 'Open in Firefly', + }; + const pill = createTag('button', { ...attrs, type: 'button', tabIndex: -1 }); + if (deeplinkUrl) { + pill.addEventListener('click', () => { + window.open(deeplinkUrl, '_blank', 'noopener'); + }); + } + + const text = createTag('span', { class: `${BLOCK}-prompt-text` }, promptText || ''); + const icon = createTag('span', { class: `${BLOCK}-prompt-icon`, 'aria-hidden': 'true' }); + icon.innerHTML = ICONS.prompt; + + pill.append(text, icon); + return pill; +} + +function createCard(item, index) { + const card = createTag('article', { class: `${BLOCK}-card` }); + card.dataset.slideIndex = String(index); + const mediaWrapper = createTag('div', { class: `${BLOCK}-media` }); + const clonedPicture = item.pictureEl.cloneNode(true); + const img = clonedPicture.querySelector('img'); + if (img) img.setAttribute('loading', 'eager'); + mediaWrapper.appendChild(clonedPicture); + card.append(mediaWrapper, createPromptPill(item.promptText || '', item.deeplinkUrl)); + return card; +} + +function createNavButton(direction, onClick) { + const isPrev = direction === NAV_DIRECTIONS.PREV; + const button = createTag('button', { + class: `${BLOCK}-nav-btn ${isPrev ? 'prev' : 'next'}`, + type: 'button', + 'aria-label': isPrev ? 'Previous slide' : 'Next slide', + }); + button.innerHTML = isPrev ? ICONS.navPrev : ICONS.navNext; + button.addEventListener('click', onClick); + return button; +} + +/** Populates the track with only authored carousel cards. */ +function buildTrack(track, items) { + const cards = items.map((item, index) => createCard(item, index)); + track.append(...cards); + return cards; +} + +function getTrackStep(cards) { + const firstCard = cards[0]; + const cardWidth = firstCard?.getBoundingClientRect().width; + if (!cardWidth) return null; + return cardWidth + GAP; +} + +function updateActiveCard(cards, currentIndex) { + cards.forEach((card) => { + const isActive = Number(card.dataset.slideIndex) === currentIndex; + card.classList.toggle('active', isActive); + card.tabIndex = isActive ? 0 : -1; + const prompt = card.querySelector(`.${BLOCK}-prompt`); + if (prompt) prompt.tabIndex = isActive ? 0 : -1; + }); +} + +function setCircularOrder(cards, currentIndex, itemCount, beforeActive) { + const startIndex = wrapIndex(currentIndex - beforeActive, itemCount); + for (let pos = 0; pos < itemCount; pos += 1) { + const realIndex = wrapIndex(startIndex + pos, itemCount); + cards[realIndex].style.order = String(pos + 1); + } +} + +function applyNavFrame(track, cards, state, itemCount, frame, animate = false) { + const step = getTrackStep(cards); + if (step === null) return; + setCircularOrder(cards, state.currentIndex, itemCount, frame.beforeActive); + updateActiveCard(cards, state.currentIndex); + track.style.transition = animate ? '' : 'none'; + track.style.transform = `translate3d(${frame.offsetMultiplier * step}px, 0, 0)`; + if (!animate) { + track.getBoundingClientRect(); + track.style.transition = ''; + } +} + +function waitForTrackTransition(track, onDone) { + let settled = false; + const settle = () => { + if (settled) return; + settled = true; + // eslint-disable-next-line no-use-before-define + clearTimeout(fallbackTimer); + onDone(); + }; + const fallbackTimer = setTimeout(settle, TRANSITION_FALLBACK_MS); + track.addEventListener('transitionend', settle, { once: true }); +} + +function updateCurrentIndex(state, itemCount, direction) { + state.currentIndex = wrapIndex( + state.currentIndex + NAV_INDEX_DELTAS[direction], + itemCount, + ); +} + +function createMoveHandler(track, itemCount, state, applyFrame) { + return (direction) => { + if (itemCount <= 1 || state.isAnimating) return false; + state.isAnimating = true; + NAV_SEQUENCES[direction].forEach(({ frame, animate }) => applyFrame(frame, animate)); + waitForTrackTransition(track, () => { + updateCurrentIndex(state, itemCount, direction); + applyFrame(NAV_FRAMES.BASE, false); + state.isAnimating = false; + }); + return true; + }; +} + +/** Creates circular prev/next navigation controls. */ +function createNavControls(track, navContainer, itemCount, state, cards) { + const applyFrame = (frame, animate = false) => applyNavFrame( + track, + cards, + state, + itemCount, + frame, + animate, + ); + const move = createMoveHandler(track, itemCount, state, applyFrame); + let prevButton; + let nextButton; + + const moveByDirection = (direction, focusButton = false) => { + const moved = move(direction); + if (!moved || !focusButton) return moved; + if (direction === NAV_DIRECTIONS.PREV) prevButton.focus(); + else nextButton.focus(); + return moved; + }; + + prevButton = createNavButton(NAV_DIRECTIONS.PREV, () => moveByDirection(NAV_DIRECTIONS.PREV)); + nextButton = createNavButton(NAV_DIRECTIONS.NEXT, () => moveByDirection(NAV_DIRECTIONS.NEXT)); + navContainer.prepend(prevButton); + navContainer.append(nextButton); + applyFrame(NAV_FRAMES.BASE, false); + return { + reposition: () => applyFrame(NAV_FRAMES.BASE, false), + moveNext: (focusButton = false) => moveByDirection(NAV_DIRECTIONS.NEXT, focusButton), + movePrev: (focusButton = false) => moveByDirection(NAV_DIRECTIONS.PREV, focusButton), + }; +} + +/** Creates the top-level structural containers for the carousel. */ +function createCarouselStructure() { + const viewport = createTag('div', { class: `${BLOCK}-viewport` }); + const track = createTag('div', { class: `${BLOCK}-track` }); + viewport.appendChild(track); + return { viewport, track }; +} + +// Keeps the track position up-to-date when the viewport +function observeResize(viewport, updateCarousel) { + const observer = new ResizeObserver(() => updateCarousel()); + observer.observe(viewport); +} + +const SWIPE_THRESHOLD_PX = 50; + +/** Enables touch swipe: left = next, right = prev. Ignores mostly-vertical drags. */ +function setupSwipe(viewport, moveNext, movePrev) { + let startX = 0; + let startY = 0; + + viewport.addEventListener('touchstart', (e) => { + if (e.changedTouches.length !== 1) return; + startX = e.changedTouches[0].clientX; + startY = e.changedTouches[0].clientY; + }, { passive: true }); + + viewport.addEventListener('touchend', (e) => { + if (e.changedTouches.length !== 1) return; + const endX = e.changedTouches[0].clientX; + const endY = e.changedTouches[0].clientY; + const deltaX = endX - startX; + const deltaY = endY - startY; + + if (Math.abs(deltaX) < SWIPE_THRESHOLD_PX) return; + if (Math.abs(deltaY) >= Math.abs(deltaX)) return; + + if (deltaX < 0) moveNext(); + else movePrev(); + }, { passive: true }); +} + +function setupArrowKeyNavigation(el, controls) { + el.addEventListener('keydown', (e) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + if (!el.contains(document.activeElement)) return; + e.preventDefault(); + if (e.key === 'ArrowLeft') controls.movePrev(true); + else controls.moveNext(true); + }); +} + +function setupAutoScroll(viewport, moveNext) { + let autoScrollTimer = null; + let autoScrollStopped = false; + + const clearAutoScroll = () => { + if (!autoScrollTimer) return; + clearInterval(autoScrollTimer); + autoScrollTimer = null; + }; + + const stopAutoScroll = () => { + if (autoScrollStopped) return; + autoScrollStopped = true; + clearAutoScroll(); + // eslint-disable-next-line no-use-before-define + observer.disconnect(); + }; + + const startAutoScroll = () => { + if (autoScrollStopped || autoScrollTimer) return; + autoScrollTimer = setInterval(() => moveNext(), AUTOSCROLL_INTERVAL_MS); + }; + + const observer = new IntersectionObserver(([entry]) => { + if (autoScrollStopped) return; + if (entry?.isIntersecting) startAutoScroll(); + else clearAutoScroll(); + }); + + observer.observe(viewport); + ['mouseenter', 'pointerdown', 'keydown', 'focusin', 'touchstart'].forEach((eventName) => { + viewport.addEventListener(eventName, stopAutoScroll, { once: true, capture: true }); + }); +} + +export default async function init(el) { + const items = parseItemsFromDOM(el); + if (!items.length) return; + + el.textContent = ''; + + const state = { currentIndex: 0, isAnimating: false }; + const structure = createCarouselStructure(); + const cards = buildTrack(structure.track, items); + const controls = createNavControls( + structure.track, + structure.viewport, + items.length, + state, + cards, + ); + + el.append(structure.viewport); + observeResize(structure.viewport, controls.reposition); + setupAutoScroll(structure.viewport, controls.moveNext); + setupSwipe(structure.viewport, controls.moveNext, controls.movePrev); + setupArrowKeyNavigation(el, controls); +} diff --git a/creativecloud/blocks/firefly-howto/firefly-howto.css b/creativecloud/blocks/firefly-howto/firefly-howto.css new file mode 100644 index 000000000..564c3b77d --- /dev/null +++ b/creativecloud/blocks/firefly-howto/firefly-howto.css @@ -0,0 +1,225 @@ +/* Tokens */ +.firefly-howto { + --c-text-primary: var(--color-black); + --c-text-tertiary: var(--color-black); + --c-bg-default: var(--color-white); + --c-bg-active: #ece4f3; + --c-focus: #0054bb; + --s-spacing-md: 1.5rem; + --s-spacing-xl: 40px; + --acc-item-radius: 8px; + --acc-item-shadow: 0 2px 8px rgb(0 0 0 / 5%); + --anim-speed: 0.4s; + --anim-easing: cubic-bezier(0.4, 0, 0.2, 1); + --foreground-gap: 1.25rem; + --icon-size: 16px; + --icon-offset: 40px; + --trigger-padding-inline: var(--s-spacing-md); + --trigger-gap: 16px; + --trigger-row-gap: 4px; + --step-column-width: 0; + --step-font-size: 0.75rem; + --title-font-size: 1.25rem; + --content-font-size: 1rem; + --content-bottom-padding: 8px; + --panel-top-offset: -16px; + --content-right-padding: calc(var(--trigger-padding-inline) + var(--icon-offset)); + --content-left-padding: var(--trigger-padding-inline); + + font-family: var(--body-font-family); +} + +/* Layout */ +.firefly-howto .foreground { + width: 100%; + max-width: 100%; + display: flex; + flex-direction: column; + gap: var(--foreground-gap); +} + +/* Accordion Item */ +.firefly-howto .firefly-howto-item { + background: var(--c-bg-default); + border-radius: var(--acc-item-radius); + box-shadow: var(--acc-item-shadow); + transition: background-color var(--anim-speed) ease; + overflow: hidden; + border: 1px solid transparent; + position: relative; + cursor: pointer; +} + +.firefly-howto .firefly-howto-item.is-open { + background-color: var(--c-bg-active); +} + +/* Trigger/Header */ +.firefly-howto .firefly-howto-header { + margin: 0; +} + +.firefly-howto .firefly-howto-trigger { + width: 100%; + display: grid; + grid-template-columns: 1fr var(--icon-size); + grid-template-areas: + "step step" + "title icon"; + row-gap: var(--trigger-row-gap); + column-gap: var(--trigger-gap); + align-items: start; + padding: var(--trigger-padding-inline); + background: none; + border: none; + text-align: left; + cursor: pointer; + color: var(--c-text-primary); + font-family: var(--body-font-family); + position: relative; + z-index: 1; + outline: none; +} + +.firefly-howto .firefly-howto-item:has(.firefly-howto-trigger:focus-visible) { + outline: 2px solid var(--c-focus); + outline-offset: -2px; +} + +.firefly-howto .step-indicator { + grid-area: step; + font-size: var(--step-font-size); + font-weight: 500; + color: var(--c-text-tertiary); + text-transform: uppercase; + line-height: 1.25; + overflow-wrap: break-word; +} + +.firefly-howto .firefly-howto-item.is-open .step-indicator { + color: var(--c-text-primary); +} + +.firefly-howto .firefly-howto-title { + grid-area: title; + font-size: var(--title-font-size); + font-weight: 800; + line-height: 1.5; +} + +/* Icon */ +.firefly-howto .firefly-howto-icon { + grid-area: icon; + align-self: center; + display: flex; + width: var(--icon-size); + height: var(--icon-size); + align-items: center; + justify-content: center; + color: var(--c-text-primary); + z-index: 3; +} + +.firefly-howto .firefly-howto-icon svg { + width: 100%; + height: 100%; +} + +.firefly-howto .firefly-howto-icon .vert-line { + transform-origin: 50% 50%; + transition: transform var(--anim-speed) var(--anim-easing), opacity var(--anim-speed) var(--anim-easing); +} + +.firefly-howto .firefly-howto-trigger[aria-expanded='true'] .firefly-howto-icon .vert-line { + transform: rotate(90deg); + opacity: 0; +} + +/* Panel + Content */ +.firefly-howto .firefly-howto-panel { + display: grid; + grid-template-rows: 0fr; + visibility: hidden; + transition: grid-template-rows var(--anim-speed) var(--anim-easing), visibility 0s linear var(--anim-speed); + position: relative; + top: var(--panel-top-offset); + z-index: 2; +} + +.firefly-howto .firefly-howto-panel-inner { + overflow: hidden; +} + +.firefly-howto .firefly-howto-content { + padding: 0 var(--content-right-padding) var(--content-bottom-padding) var(--content-left-padding); + opacity: 0; + transition: opacity 0.2s ease; + font-weight: 400; + font-size: var(--content-font-size); + letter-spacing: normal; + line-height: 1.5; +} + +.firefly-howto .firefly-howto-content > *:first-child { + margin-top: 0; +} + +.firefly-howto .firefly-howto-content > *:last-child { + margin-bottom: 0; +} + +.firefly-howto .foreground .firefly-howto-panel .firefly-howto-content p { + line-height: 1.6; + color: var(--c-text-primary); + margin: 0; +} + +/* State Variants */ +.firefly-howto .firefly-howto-item.is-open .firefly-howto-panel { + grid-template-rows: 1fr; + visibility: visible; + transition: grid-template-rows var(--anim-speed) var(--anim-easing), visibility 0s linear 0s; +} + +.firefly-howto .firefly-howto-item.is-open .firefly-howto-content { + opacity: 1; + transition: opacity var(--anim-speed) ease 0.1s; +} + +/* Responsive Overrides */ +@media (min-width: 600px) { + .firefly-howto { + --step-column-width: 55px; + --step-font-size: 1rem; + --trigger-padding-inline: var(--s-spacing-xl); + --trigger-row-gap: 0; + --content-bottom-padding: 16px; + --panel-top-offset: -24px; + --content-left-padding: calc(var(--trigger-padding-inline) + var(--step-column-width) + var(--trigger-gap)); + } + + .firefly-howto .firefly-howto-trigger { + grid-template-columns: var(--step-column-width) 1fr var(--icon-size); + grid-template-areas: "step title icon"; + align-items: baseline; + } +} + +@media (min-width: 1200px) { + .firefly-howto { + --step-column-width: 65px; + --trigger-gap: var(--s-spacing-md); + --title-font-size: 1.5rem; + --content-font-size: 1.25rem; + --content-left-padding: calc(var(--trigger-padding-inline) + var(--step-column-width) + var(--trigger-gap)); + } +} + +/* Reduced Motion */ +@media (prefers-reduced-motion: reduce) { + .firefly-howto .firefly-howto-icon .vert-line, + .firefly-howto .firefly-howto-panel, + .firefly-howto .firefly-howto-content { + transition: none !important; + } +} diff --git a/creativecloud/blocks/firefly-howto/firefly-howto.js b/creativecloud/blocks/firefly-howto/firefly-howto.js new file mode 100644 index 000000000..6f360564b --- /dev/null +++ b/creativecloud/blocks/firefly-howto/firefly-howto.js @@ -0,0 +1,273 @@ +import { createTag } from '../../scripts/utils.js'; + +// ===== CONFIG ===== + +const CONFIG = { + DEFAULT_STEP_PREFIX: 'Step', + SEO_MARKER: 'seo-props', + STEP_TITLE_DELIMITER: '||', + CLICK_DRAG_THRESHOLD: 5, +}; + +const LANA_OPTIONS = { tags: 'firefly-howto', errorType: 'i' }; + +const ICON_SVG = ''; + +// ===== LOGGING ===== + +function logError(message, error) { + window.lana?.log(`${message}: ${error}`, LANA_OPTIONS); +} + +// ===== UTILITY FUNCTIONS ===== + +function parseStepTitle(rawTitle) { + let stepPrefix = CONFIG.DEFAULT_STEP_PREFIX; + let title = rawTitle; + let fullTitle = title; + + if (rawTitle.includes(CONFIG.STEP_TITLE_DELIMITER)) { + const parts = rawTitle.split(CONFIG.STEP_TITLE_DELIMITER); + stepPrefix = parts[0]?.trim() || CONFIG.DEFAULT_STEP_PREFIX; + title = parts[1]?.trim() || ''; + fullTitle = `${stepPrefix} ${title}`; + } + + return { stepPrefix, title, fullTitle }; +} + +// ===== PARSING (SEO PROPS) ===== + +function parseCsv(value) { + return value.split(',').map((item) => item.trim()).filter(Boolean); +} + +function extractSeoData(item) { + const seoData = {}; + const props = item.children[1]?.querySelectorAll('p') || []; + + props.forEach((p) => { + const text = p.textContent; + const splitIndex = text.indexOf(':'); + + if (splitIndex < 0) return; + + const key = text.substring(0, splitIndex).trim().toLowerCase(); + const value = text.substring(splitIndex + 1).trim(); + + if (key === 'name') seoData.name = value; + if (key === 'description') seoData.description = value; + if (key === 'total-time' || key === 'totaltime') seoData.totalTime = value; + if (key === 'tools' || key === 'tool') seoData.tools = parseCsv(value); + if (key === 'supply' || key === 'supplies') seoData.supply = parseCsv(value); + }); + + return seoData; +} + +// ===== PARSING (STEPS) ===== + +function parseStep(item, index) { + const innerDiv = item.querySelector('div'); + if (!innerDiv) return null; + + const titleEl = innerDiv.querySelector('h3'); + const contentEl = innerDiv.querySelector('p'); + const titleText = titleEl ? titleEl.innerText : ''; + const { stepPrefix, title, fullTitle } = parseStepTitle(titleText); + + return { + id: index, + title, + fullTitle, + content: contentEl ? contentEl.innerHTML : '', + plainText: contentEl ? contentEl.textContent : '', + headingId: titleEl ? titleEl.id : `acc-heading-${index}`, + stepPrefix, + }; +} + +function extractData(el) { + const steps = []; + let seoData = {}; + const isSeoEnabled = el.classList.contains('seo'); + + Array.from(el.children).forEach((item, index) => { + const innerDivs = Array.from(item.children); + if (innerDivs.length === 0) return; + + const isSeoMarker = innerDivs[0].textContent.trim() === CONFIG.SEO_MARKER; + if (isSeoMarker) { + if (isSeoEnabled && innerDivs.length > 1) { + seoData = { ...seoData, ...extractSeoData(item) }; + } + return; + } + + const step = parseStep(item, index); + if (step) steps.push(step); + }); + + return { steps, seoData }; +} + +// ===== INTERACTION ===== + +function handlePointerDown(e) { + const item = e.currentTarget; + item.dataset.startX = e.clientX; + item.dataset.startY = e.clientY; +} + +function isDragClick(e, item) { + const startX = parseFloat(item.dataset.startX || 0); + const startY = parseFloat(item.dataset.startY || 0); + + if (e.clientX !== 0 || e.clientY !== 0) { + const diffX = Math.abs(e.clientX - startX); + const diffY = Math.abs(e.clientY - startY); + return diffX > CONFIG.CLICK_DRAG_THRESHOLD || diffY > CONFIG.CLICK_DRAG_THRESHOLD; + } + + return false; +} + +function setExpandedState(item, trigger, expanded) { + trigger.setAttribute('aria-expanded', expanded ? 'true' : 'false'); + item.classList.toggle('is-open', expanded); +} + +function handleItemClick(e) { + const item = e.currentTarget; + if (isDragClick(e, item)) return; + + const interactiveTarget = e.target?.closest?.('a, input'); + if (interactiveTarget) return; + + const trigger = item.querySelector('.firefly-howto-trigger'); + if (!trigger) return; + + const clickedTrigger = e.target?.closest?.('.firefly-howto-trigger'); + const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; + + if (isExpanded) { + setExpandedState(item, trigger, false); + } else { + if (!clickedTrigger) return; + setExpandedState(item, trigger, true); + } +} + +// ===== DOM BUILDING ===== + +function createAccordionItem(data) { + const { title, content, headingId, stepPrefix } = data; + const isOpen = false; + + const itemWrapper = createTag('div'); + itemWrapper.className = `firefly-howto-item ${isOpen ? 'is-open' : ''}`; + + itemWrapper.addEventListener('pointerdown', handlePointerDown); + itemWrapper.addEventListener('click', handleItemClick); + const header = createTag('h3', { class: 'firefly-howto-header' }); + + const btn = createTag('button', { class: 'firefly-howto-trigger', id: `btn-${headingId}`, 'aria-expanded': isOpen, type: 'button', 'aria-controls': `panel-${headingId}` }); + + const titleSpan = createTag('span', { class: 'firefly-howto-title' }); + titleSpan.textContent = title; + + const iconSpan = createTag('span', { class: 'firefly-howto-icon' }); + iconSpan.innerHTML = ICON_SVG; + + const stepSpan = createTag('span', { class: 'step-indicator' }); + stepSpan.textContent = `${stepPrefix} `; + + btn.appendChild(stepSpan); + btn.appendChild(titleSpan); + btn.appendChild(iconSpan); + header.appendChild(btn); + + const panel = createTag('div', { class: 'firefly-howto-panel', id: `panel-${headingId}`, role: 'region', 'aria-labelledby': `btn-${headingId}` }); + const panelInner = createTag('div', { class: 'firefly-howto-panel-inner' }); + const contentWrapper = createTag('div', { class: 'firefly-howto-content' }); + + const p = createTag('p'); + p.innerHTML = content; + + contentWrapper.appendChild(p); + panelInner.appendChild(contentWrapper); + panel.appendChild(panelInner); + + itemWrapper.appendChild(header); + itemWrapper.appendChild(panel); + + return itemWrapper; +} + +// ===== SEO SCHEMA ===== + +function buildSchema(steps, seoData) { + const schema = { + '@context': 'https://schema.org', + '@type': 'HowTo', + name: seoData.name, + description: seoData.description, + step: steps.map((item) => ({ + '@type': 'HowToStep', + name: item.fullTitle, + text: item.plainText, + })), + }; + + if (seoData.totalTime) { + schema.totalTime = seoData.totalTime; + } + + if (seoData.tools && seoData.tools.length > 0) { + schema.tool = seoData.tools.map((tool) => ({ + '@type': 'HowToTool', + name: tool, + })); + } + + if (seoData.supply && seoData.supply.length > 0) { + schema.supply = seoData.supply.map((supply) => ({ + '@type': 'HowToSupply', + name: supply, + })); + } + + return schema; +} + +// ===== ORCHESTRATION ===== + +function decorateContent(el) { + const { steps, seoData } = extractData(el); + const foreground = createTag('div', { class: 'foreground' }); + + steps.forEach((itemData) => { + const itemNode = createAccordionItem(itemData); + foreground.appendChild(itemNode); + }); + + el.innerHTML = ''; + el.appendChild(foreground); + + if (el.classList.contains('seo') && seoData.name && seoData.description) { + const schema = buildSchema(steps, seoData); + const script = createTag('script', { type: 'application/ld+json' }, JSON.stringify(schema)); + document.head.appendChild(script); + } +} + +// ===== PUBLIC INIT ===== + +export default function init(el) { + try { + el.classList.add('con-block'); + decorateContent(el); + } catch (err) { + logError('Firefly accordion init error', err); + } +} diff --git a/creativecloud/blocks/firefly-model-showcase/firefly-model-showcase.css b/creativecloud/blocks/firefly-model-showcase/firefly-model-showcase.css index ca2769822..eabe10143 100644 --- a/creativecloud/blocks/firefly-model-showcase/firefly-model-showcase.css +++ b/creativecloud/blocks/firefly-model-showcase/firefly-model-showcase.css @@ -201,6 +201,12 @@ padding: 12px; } } +@media screen and (max-width: 360px) { + .firefly-model-showcase-content .action-area a:first-child:nth-last-child(2), + .firefly-model-showcase-content .action-area a:first-child:nth-last-child(2) ~ a { + white-space: nowrap; + } +} /* End of second CTA support */ diff --git a/creativecloud/blocks/firefly-share/firefly-share.css b/creativecloud/blocks/firefly-share/firefly-share.css new file mode 100644 index 000000000..d6a739932 --- /dev/null +++ b/creativecloud/blocks/firefly-share/firefly-share.css @@ -0,0 +1,276 @@ +.firefly-share { + --svg-icon-dimension: 24px; + --list-gap: 28px; + --copy-tooltip-background-color: #6D6D6D; + margin-left: auto; + margin-right: auto; + text-align: center; + overflow: hidden; +} + +.firefly-share.inline { + overflow: unset; +} + +.firefly-share.dark { + background-color: #1E1E1E; +} + +.firefly-share svg.icon { + height: var(--svg-icon-dimension); + transition: filter 0.3s; + display: inline-block; + width: var(--svg-icon-dimension); + margin: 0; + color: #222; +} + +.firefly-share ul.icon-container, +.firefly-share ul.icon-container li, +.firefly-share ul.icon-container li a { + display: flex; + align-items: center; + padding-bottom: 0; + justify-content: center; + flex-wrap: wrap; +} + +.firefly-share ul.icon-container { + list-style: none; + gap: var(--list-gap); + margin: 0; + padding: 1rem 0; +} + +.firefly-share :is([role="heading"], h1, h2, h3, h4, h5, h6) { + margin-top: 2.5rem; + margin-bottom: 2.75rem; +} + + +.firefly-share.xxl-heading :is([role="heading"], h1, h2, h3, h4, h5, h6) { + font-size: 1.5rem; + font-weight: 900; + letter-spacing: -4.5%; +} + +.firefly-share.dark :is([role="heading"], h1, h2, h3, h4, h5, h6), +.firefly-share.dark ul.icon-container svg { + color: var(--color-white); +} + +.firefly-share ul.icon-container svg { + color: #222; +} + +.firefly-share li:has(.copy-to-clipboard) { + position: relative; +} + +.firefly-share li:has(.copy-to-clipboard:not(.hide-copy-tooltip))::before { + content: ''; + width: var(--svg-icon-dimension); + height: 5px; + position: absolute; + bottom: 100%; +} + +.firefly-share .copy-to-clipboard { + padding: 0; + background: none; + margin: 0; + line-height: 0; + cursor: pointer; + position: relative; + transition: filter 0.3s; + border: none; + margin-left: -2px; +} + +.firefly-share .icon-container li > :is(a, button) { + outline-offset: 1px; +} + +.firefly-share :is(a, .copy-to-clipboard) svg.icon:hover { + filter: brightness(0.7); +} + +.firefly-share .copy-to-clipboard::before, +.firefly-share .copy-to-clipboard::after { + display: block; + position : absolute; + bottom: 100%; + transition: transform 130ms ease-in-out, opacity 130ms ease-in-out, visibility 0ms linear 130ms; + transition-delay: 0ms; + background-color: var(--copy-tooltip-background-color); + visibility: visible; + opacity: 1; + cursor: default; +} + +.firefly-share .copy-to-clipboard::before { + content: attr(aria-label); + bottom: 100%; + left: -45%; + transform: translate(-50%, -4px); + color: var(--color-white); + padding: 4px 9px 5px; + font-size: 12px; + line-height: 1.3; + border-radius: 4px; + white-space: nowrap; +} + +.firefly-share .copy-to-clipboard::after { + content: ''; + left: 50%; + transform: translate(-50%, 0); + height: 4px; + width: 8px; + clip-path: polygon(0 -5%, 50% 100%, 100% -5%); +} + +.firefly-share .copy-to-clipboard.hide-copy-tooltip::before, +.firefly-share .copy-to-clipboard.hide-copy-tooltip::after { + visibility: hidden; + opacity: 0; +} + +[dir="rtl"] .firefly-share ul.icon-container { + padding-right: 0; +} + +[dir="rtl"] .firefly-share .copy-to-clipboard { + margin-left: 0; + margin-right: -2px; +} + +[dir="rtl"] .firefly-share .copy-to-clipboard::before { + left: 150%; +} + +.firefly-share.inline .copy-to-clipboard::after { + z-index: 8; +} + +.firefly-share .copy-to-clipboard:not(.hide-copy-tooltip)::before { + transform: translate(-50%, -6px); +} + +.firefly-share .copy-to-clipboard:not(.hide-copy-tooltip)::after { + transform: translate(-50%, -2px); +} + +.firefly-share .copy-to-clipboard.copy-to-clipboard-copied::before { + content: attr(data-copied); +} + +.firefly-share.inline { + margin: 0 auto; + text-align: center; + padding: 8px 0 16px; +} + +.firefly-share.inline ul.icon-container { + justify-content: flex-start; + margin: 0; +} + +main > .section > firefly-share.inline { + margin: 0 auto; +} + +main > .section > .inline-wrapper > firefly-share.inline { + margin-top: 0; + margin-bottom: 0; +} + +main > .section.center firefly-share.inline ul.icon-container { + margin: 0 auto; + justify-content: center; +} + +@media screen and (min-width: 360px) { + .firefly-share.left-mobile .tracking-header { + max-width: var(--grid-container-width); + margin: 0 auto; + } + + .firefly-share.left-mobile :is([role="heading"], h1, h2, h3, h4, h5, h6) { + text-align: start; + } + + .firefly-share.left-mobile ul.icon-container { + max-width: calc(var(--grid-container-width) - 2px); + margin-left : auto; + margin-right: auto; + justify-content: space-around; + } +} + +@media screen and (min-width: 600px) { + .firefly-share.left-mobile .tracking-header { + max-width: 100%; + margin: 0; + } + + .firefly-share.left-mobile :is([role="heading"], h1, h2, h3, h4, h5, h6) { + text-align: center; + } + + .firefly-share.left-mobile ul.icon-container { + max-width: 100%; + justify-content: center; + } + + .firefly-share :is([role="heading"], h1, h2, h3, h4, h5, h6) { + margin-top: 1.25rem; + margin-bottom: 1.25rem; + } + + .firefly-share .copy-to-clipboard::before, + [dir="rtl"] firefly-share .copy-to-clipboard::before { + left: 50%; + } +} + +.firefly-share .aria-live-container { + position: absolute; + width: 0; + height: 0; + overflow: hidden; +} + +@media (min-width: 900px){ + .firefly-share { + --svg-icon-dimension: 40px; + --list-gap: 56px; + } + + .firefly-share .copy-to-clipboard::before { + content: attr(aria-label); + bottom: 100%; + left: -45%; + transform: translate(-50%, -4px); + color: var(--color-white); + padding: 4px 9px 5px; + font-size: 16px; + line-height: 1.3; + border-radius: 4px; + white-space: nowrap; + } + + .firefly-share :is([role="heading"], h1, h2, h3, h4, h5, h6) { + margin-top: 2.5rem; + margin-bottom: 2.5rem; + } + + .firefly-share.xxl-heading :is([role="heading"], h1, h2, h3, h4, h5, h6) { + font-size: 40px; + font-weight: 900; + } + + .firefly-share ul.icon-container { + padding: 2rem 0 + } +} diff --git a/creativecloud/blocks/firefly-share/firefly-share.js b/creativecloud/blocks/firefly-share/firefly-share.js new file mode 100644 index 000000000..655f65f5a --- /dev/null +++ b/creativecloud/blocks/firefly-share/firefly-share.js @@ -0,0 +1,229 @@ +import { createTag, getConfig, getLibs } from '../../scripts/utils.js'; + +const FIREFLY_SHARE = '/cc-shared/assets/svg/firefly-share.svg'; + +export async function getSVGsfromFile(path, selectors) { + if (!path) return null; + const resp = await fetch(path); + if (!resp.ok) return null; + + const text = await resp.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, 'image/svg+xml'); + + if (!selectors) { + const svg = doc.querySelector('svg'); + if (svg) return [{ svg }]; + /* c8 ignore next 4 */ + return null; + } + if (!(selectors instanceof Array)) { + /* eslint-disable no-param-reassign */ + selectors = [selectors]; + } + + return selectors.map((selector) => { + const symbol = doc.querySelector(`#${selector}`); + if (!symbol) return null; + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + while (symbol.firstChild) svg.appendChild(symbol.firstChild); + [...symbol.attributes].forEach((attr) => svg.attributes.setNamedItem(attr.cloneNode())); + svg.classList.add('icon'); + svg.classList.add(`icon-${selector}`); + svg.removeAttribute('id'); + return { svg, name: selector }; + }); +} + +function getPlatforms(el) { + const manualShares = el.querySelectorAll('a'); + if (manualShares.length === 0) return ['facebook', 'x', 'linkedin', 'pinterest', 'reddit']; + const platforms = []; + [...manualShares].forEach((share) => { + const { href } = share; + const url = new URL(href); + const parts = url.host.split('.'); + const platform = parts[parts.length - 2] !== 'twitter' ? parts[parts.length - 2] : 'x'; + platforms.push(platform); + const parentP = share.closest('p'); + parentP?.remove(); + }); + return platforms; +} + +function getPrevHeadingLevel(block) { + const prevHeading = [...document.querySelectorAll('h2, h3, h4, h5, h6')] + .reverse() + /* eslint-disable-next-line no-bitwise */ + .find((heading) => heading.compareDocumentPosition(block) & Node.DOCUMENT_POSITION_FOLLOWING); + const prevHeadingTag = prevHeading?.tagName.toLowerCase() ?? 'h2'; + return prevHeadingTag.replace('h', ''); +} + +function toSentenceCase(str) { + if (!str || typeof str !== 'string') return ''; + /* eslint-disable-next-line no-useless-escape */ + return str.toLowerCase().replace(/(^\s*\w|[\.\!\?]\s*\w)/g, (c) => c.toUpperCase()); +} + +function showCopyTooltip({ copyButton, show = true, copied = false }) { + copyButton.classList.toggle('hide-copy-tooltip', !show); + copyButton.classList.toggle('copy-to-clipboard-copied', copied); +} + +export default async function decorate(block) { + const config = getConfig(); + const miloLibs = getLibs('/libs'); + const { inlineBlock } = await import(`${miloLibs}/utils/inline.js`); + const { replaceKey } = await import(`${miloLibs}/features/placeholders.js`); + const { loadStyle } = await import(`${miloLibs}/utils/utils.js`); + + loadStyle(`${miloLibs}/styles/inline.css`); + const platforms = getPlatforms(block); + let firstRow = block.querySelector(':scope > div'); + let headingElem; + + if (block.matches('.inline')) firstRow?.remove(); + else if (firstRow?.textContent.trim().length) { + headingElem = firstRow.querySelector('p, div:not(:has(p, h1, h2, h3, h4, h5, h6))'); + } else { + headingElem = createTag('p', null, toSentenceCase(await replaceKey('share-this-page', config))); + if (firstRow) firstRow.replaceChildren(headingElem); + else { + firstRow = createTag('div', null, headingElem); + block.append(firstRow); + } + } + + firstRow?.classList.add('tracking-header'); + headingElem?.setAttribute('role', 'heading'); + headingElem?.setAttribute('aria-level', getPrevHeadingLevel(block)); + + const clipboardSupport = !!navigator.clipboard; + if (clipboardSupport) platforms.push('clipboard'); + const svgs = await getSVGsfromFile( + FIREFLY_SHARE, + platforms, + ); + if (!svgs) return; + + const shareToText = toSentenceCase(await replaceKey('share-to', config)); + const url = encodeURIComponent(window.location.href); + const title = document.title ?? url; + /* eslint-disable no-shadow */ + const getDetails = (name, url) => { + switch (name) { + case 'facebook': + return { + title: 'Facebook', + href: `https://www.facebook.com/sharer/sharer.php?u=${url}`, + }; + case 'x': + return { + title: 'X', + href: `https://x.com/share?&url=${url}`, + }; + case 'linkedin': + return { + title: 'LinkedIn', + href: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`, + }; + case 'pinterest': + return { + title: 'Pinterest', + href: `https://pinterest.com/pin/create/button/?url=${url}&description=${title}`, + }; + case 'reddit': + return { + title: 'Reddit', + href: `https://reddit.com/submit?url=${url}&title=${title}`, + }; + default: + /* c8 ignore next 1 */ + return null; + } + }; + + const container = createTag('ul', { class: 'icon-container' }); + svgs.forEach(async (svg) => { + if (svg.name === 'clipboard') return; + + const obj = getDetails(svg.name, url); + if (!obj) return; + + const shareLink = createTag( + 'a', + { + title: `${shareToText} ${obj.title}`, + target: '_blank', + href: obj.href, + }, + svg.svg, + ); + const li = createTag('li'); + li.appendChild(shareLink); + container.append(li); + shareLink.addEventListener('click', (e) => { + /* c8 ignore next 2 */ + e.preventDefault(); + window.open(shareLink.href, 'newwindow', 'width=600, height=400'); + }); + }); + + const clipboardSvg = svgs.find((svg) => svg.name === 'clipboard'); + if (clipboardSvg && clipboardSupport) { + const clipboardToolTip = toSentenceCase( + await replaceKey('copy-to-clipboard', config), + ); + const copiedTooltip = toSentenceCase(await replaceKey('copied', config)); + const copyButton = createTag( + 'button', + { + type: 'button', + class: 'copy-to-clipboard hide-copy-tooltip', + 'aria-label': clipboardToolTip, + 'data-copied': `${copiedTooltip}!`, + }, + clipboardSvg.svg, + ); + + let clipboardTimeout; + const li = createTag('li'); + ['keydown', 'focus', 'blur', 'mouseenter', 'mouseleave'].forEach((eventType) => { + const target = eventType.startsWith('mouse') ? li : copyButton; + target.addEventListener(eventType, (event) => { + const { type, key } = event; + if (type === 'keydown' && key !== 'Escape') return; + showCopyTooltip({ copyButton, show: !['mouseleave', 'blur', 'keydown'].includes(type) }); + clearTimeout(clipboardTimeout); + }); + }); + + const copyAriaLive = createTag( + 'div', + { + 'aria-live': 'polite', + role: 'status', + class: 'aria-live-container', + }, + ); + li.append(copyButton, copyAriaLive); + container.append(li); + let changeText = false; + copyButton.addEventListener('click', (e) => { + /* c8 ignore next 6 */ + e.preventDefault(); + copyAriaLive.textContent = ''; + navigator.clipboard.writeText(window.location.href).then(() => { + showCopyTooltip({ copyButton, copied: true }); + copyAriaLive.textContent = copiedTooltip + (changeText ? '\u200b' : ''); + changeText = !changeText; + clipboardTimeout = setTimeout(() => { + showCopyTooltip({ copyButton, show: false }); + }, 2000); + }); + }); + } + block.append(container); + inlineBlock(block); +} diff --git a/creativecloud/blocks/sidenav/sidenav.js b/creativecloud/blocks/sidenav/sidenav.js index 3fe71034a..07b5bf8a9 100644 --- a/creativecloud/blocks/sidenav/sidenav.js +++ b/creativecloud/blocks/sidenav/sidenav.js @@ -22,6 +22,83 @@ const computeDaaLLText = (text) => { const generateDaaLL = (text, headline) => `${text}--${headline}`; +const getMasLibsBaseUrl = () => { + const urlParams = new URLSearchParams(window.location.search); + const masLibs = urlParams.get('maslibs'); + + if (!masLibs || masLibs.trim() === '') return null; + + const sanitized = masLibs.trim().toLowerCase(); + + if (sanitized === 'local') { + return 'http://localhost:3000'; + } + + if (sanitized === 'main') { + return 'https://main--mas--adobecom.aem.live'; + } + + const { hostname } = window.location; + const extension = hostname.endsWith('.page') ? 'page' : 'live'; + + let branch = sanitized; + if (!sanitized.includes('--')) { + branch = `${sanitized}--mas--adobecom`; + } + + return `https://${branch}.aem.${extension}`; +}; + +const getMasLibs = () => { + const baseUrl = getMasLibsBaseUrl(); + if (!baseUrl) return null; + return `${baseUrl}/web-components/dist`; +}; + +const getMasComponentUrl = (componentName, masLibsBase, hostname) => { + if (masLibsBase) { + return `${masLibsBase}/${componentName}.js`; + } + const isAdobeProd = hostname === 'www.adobe.com'; + return isAdobeProd + ? `https://www.adobe.com/mas/libs/${componentName}.js` + : `https://main--mas--adobecom.aem.live/web-components/dist/${componentName}.js`; +}; + +const failedExternalLoads = new Set(); +const loadingPromises = new Map(); + +const loadMasComponent = async (componentName) => { + if (loadingPromises.has(componentName)) { + return loadingPromises.get(componentName); + } + + if (customElements.get(componentName)) { + return Promise.resolve(); + } + + const loadPromise = (async () => { + const masLibsBase = getMasLibs(); + const targetUrl = getMasComponentUrl(componentName, masLibsBase, window.location.hostname); + + if (failedExternalLoads.has(targetUrl)) { + throw new Error(`Previously failed to load component from ${targetUrl}`); + } + + try { + return await import(targetUrl); + } catch (error) { + failedExternalLoads.add(targetUrl); + throw error; + } + })(); + + loadingPromises.set(componentName, loadPromise); + loadPromise.finally(() => loadingPromises.delete(componentName)); + + return loadPromise; +}; + const getCategories = (items, isMultilevel, mapCategories) => { const configuration = { manageTabIndex: true }; if (isMultilevel) { @@ -169,7 +246,7 @@ export default async function init(el) { decorateLinks(el); const [mainRow, categoryRow] = Array.from(el.children); const deps = Promise.all([ - import(`${libs}/deps/mas/merch-sidenav.js`), + loadMasComponent('merch-sidenav'), // eslint-disable-next-line import/no-unresolved, import/no-absolute-path import(`${libs}/deps/lit-all.min.js`), import(`${libs}/features/spectrum-web-components/dist/theme.js`), diff --git a/creativecloud/blocks/upload-marquee/upload-marquee.css b/creativecloud/blocks/upload-marquee/upload-marquee.css new file mode 100644 index 000000000..fe7a94e0f --- /dev/null +++ b/creativecloud/blocks/upload-marquee/upload-marquee.css @@ -0,0 +1,450 @@ +/* ===== BASE / TOKENS ===== */ +.upload-marquee-block { + --um-white: #fff; + --um-heading-text: #f2f2f2; + --um-body-muted: #dbdbdb; + --um-accent: #5681ff; + --um-accent-focus: #7f96ff; + --um-accent-strong: #3b63fb; + --um-accent-link: #1473e6; + --um-hover-bg: #0f1c52; + --um-radius-sm: 8px; + --um-radius-md: 20px; + --um-radius-pill: 999px; + --um-shadow-media: 0 2px 12px rgb(0 0 0 / 8%); + --um-shadow-layout: 0 4px 24px rgb(0 0 0 / 12%); + --um-text-shadow: 0 4px 39px rgb(0 0 0 / 25%); + --um-drop-zone-border: 2px dashed rgb(255 255 255 / 50%); + --um-cta-border: 2px solid rgb(255 255 255 / 35%); + + position: relative; + overflow: hidden; +} + +.upload-marquee-block .hide { display: none; } + +/* ===== BUTTON ===== */ +.upload-marquee-block .upload-action-container { + margin: 0 auto; +} + +.upload-marquee-block .action-button { + border-radius: var(--um-radius-pill); + border: 0; + background: var(--um-accent-strong); + color: var(--um-white); + cursor: pointer; + gap: 8px; + display: flex; + align-items: center; + justify-content: center; + min-height: 50px; + padding: 12px 24px 14px; + font-size: 19px; + font-weight: 700; + line-height: 24px; + text-decoration: none; +} + +.upload-marquee-block .action-button > picture { + margin-inline-end: 0; +} + +.upload-marquee-block .action-button > picture, +.upload-marquee-block .action-button > picture > img { + width: 24px; + height: 24px; +} + +.upload-marquee-block .action-button .icon-upload-to-cloud-outline svg { + height: 1.3em; + width: 1.3em; + line-height: 0; +} + +.upload-marquee-block .action-button:focus-visible { + background: var(--um-accent-strong); + color: var(--um-white); + text-decoration: none; +} + +/* ===== LAYOUT ===== */ +.upload-marquee-block .upload-marquee-layout { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + margin: 0 auto; + overflow: hidden; +} + +.upload-marquee-block .upload-marquee-left { + background: inherit; + color: var(--um-white); + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 48px var(--grid-margins-width) 8px; +} + +.upload-marquee-block .upload-marquee-right { + height: auto; + background: inherit; + display: flex; + align-items: center; + justify-content: center; + padding: 32px var(--grid-margins-width); + box-sizing: border-box; +} + +.upload-marquee-block .upload-marquee-media { + width: 100%; + align-self: center; +} + +.upload-marquee-block .upload-marquee-right .media-container { + height: 100%; + width: 100%; + border-radius: var(--um-radius-sm); + overflow: hidden; + box-shadow: var(--um-shadow-media); +} + +.upload-marquee-block .upload-marquee-right .media-container .video-container { + width: 100%; + height: 100%; +} + +.upload-marquee-block .upload-marquee-uploads > .tablet-up, +.upload-marquee-block .upload-marquee-uploads > .tablet-up.desktop-up, +.upload-marquee-block .upload-marquee-uploads > .desktop-up, +.upload-marquee-block .upload-marquee-media > .tablet-up, +.upload-marquee-block .upload-marquee-media > .tablet-up.desktop-up, +.upload-marquee-block .upload-marquee-media > .desktop-up { + display: none; +} + +/* ===== CONTENT ===== */ +.upload-marquee-block .upload-marquee-content { + display: flex; + flex-direction: column; + text-align: left; + align-items: flex-start; +} + +.upload-marquee-block .upload-marquee-content p, +.upload-marquee-block .upload-marquee-content h1, +.upload-marquee-block .upload-marquee-content h2 { + color: inherit; + margin: 0; + text-align: left; +} + +.upload-marquee-block .upload-marquee-content p:first-child { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: var(--type-body-s-size); + line-height: var(--type-body-s-lh); + margin-bottom: 12px; +} + +.upload-marquee-block .upload-marquee-content .upload-marquee-branding { + margin-bottom: 20px; + padding-bottom: 0; +} + +.upload-marquee-block .upload-marquee-content .upload-marquee-branding-row { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.upload-marquee-block .upload-marquee-content .upload-marquee-branding-first picture, +.upload-marquee-block .upload-marquee-content .upload-marquee-branding-second picture { + display: block; +} + +.upload-marquee-block .upload-marquee-content .upload-marquee-branding-first img, +.upload-marquee-block .upload-marquee-content .upload-marquee-branding-second img { + width: auto; + height: 30px; + display: block; + object-fit: contain; +} + +.upload-marquee-block .upload-marquee-content p:first-child picture, +.upload-marquee-block .upload-marquee-content p:first-child img { + height: 26px; + display: block; +} + +.upload-marquee-block .upload-marquee-content > *:first-child > picture img, +.upload-marquee-block .upload-marquee-content > *:first-child > picture svg { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: inherit; +} + +.upload-marquee-block .upload-marquee-content p.upload-marquee-branding .upload-marquee-branding-second picture { + height: 15.5px; + margin-top: 2.75px; +} + +.upload-marquee-block .upload-marquee-content p.upload-marquee-branding .upload-marquee-branding-second img { + height: 15.5px; +} + +.upload-marquee-block .upload-marquee-content a { + color: var(--um-white); +} + +.upload-marquee-block .upload-marquee-content .upload-marquee-cta { + align-self: flex-start; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 9px 18px 10px; + background: transparent; + color: var(--um-white); + border: var(--um-cta-border); + border-radius: var(--um-radius-pill); + box-sizing: border-box; + font-size: 17px; + font-weight: 700; + line-height: 21px; + text-decoration: none; + transition: border-color 0.2s ease; +} + +.upload-marquee-block .upload-marquee-content p:last-of-type { + margin-bottom: 40px; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .upload-marquee-content h1 { + color: var(--um-white); + text-shadow: var(--um-text-shadow); + font-family: var(--body-font-family); + font-size: 40px; + font-style: normal; + font-weight: 800; + line-height: 98%; + letter-spacing: -1.8px; + margin-bottom: 16px; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .upload-marquee-content p { + color: var(--um-white); + text-shadow: var(--um-text-shadow); + font-family: var(--body-font-family); + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 136%; + letter-spacing: var(--letter-spacing, 0); + margin-bottom: 16px; +} + +/* ===== DROP ZONE ===== */ +.upload-marquee-block .drop-zone-container { + background: transparent; + box-shadow: none; + padding: 0; + margin: 0; + max-width: none; +} + +.upload-marquee-block .drop-zone { + border: var(--um-drop-zone-border); + border-radius: var(--um-radius-sm); + padding: 34px 24px; + margin: 0; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + box-sizing: border-box; + background-color: inherit; + background-image: radial-gradient(circle, rgb(255 255 255 / 18%) 1px, transparent 1px); + background-size: 26px 26px; +} + +.upload-marquee-block .drop-zone:hover, +.upload-marquee-block .drop-zone.active { + background-image: none; + cursor: pointer; + border: 2px solid var(--um-accent); + background-color: var(--um-hover-bg); +} + +.upload-marquee-block .drop-zone:focus-visible { + outline: 2px solid var(--um-accent-focus); + outline-offset: 2px; + border-color: var(--um-accent); +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone > p:first-child { + color: var(--um-white); + margin: 0; + text-align: center; + font-size: var(--type-body-m-size); +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone > p:first-child img, +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone > p:first-child svg { + display: block; + margin: 0 auto; + width: 40px; + height: 40px; + opacity: 0.9; +} + +.upload-marquee-block .drop-zone .upload-action-container { + margin: 4px 0 2px; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone-container > p:last-child { + font-size: var(--type-body-xs-size); + line-height: var(--type-body-xs-lh); + margin-top: 16px; + color: var(--um-white); + text-align: center; + margin-block-end: 0; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone > p:not(:first-child):not(:last-child):not(.upload-action-container) { + color: var(--um-white); + margin: 0; + text-align: center; + line-height: 1.4; +} + +.upload-marquee-block .drop-zone > .drop-zone-heading { + color: var(--um-heading-text); + font-size: 21px; + font-weight: 700; + line-height: 20px; + letter-spacing: 0; + margin: 0; + text-align: center; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone > .drop-zone-body { + color: var(--um-body-muted); + font-size: 16px; + font-weight: 400; + line-height: 24px; + max-width: 380px; + margin: 0; + text-align: center; +} + +.upload-marquee-block .upload-marquee-layout .upload-marquee-left .drop-zone-container > p:last-child a { + color: var(--um-accent-link); + text-decoration: underline; +} + +/* ===== MEDIA ASSETS ===== */ +.upload-marquee-block .foreground .upload-marquee-layout .upload-marquee-right .media-container picture, +.upload-marquee-block .foreground .upload-marquee-layout .upload-marquee-right .media-container picture > img, +.upload-marquee-block .foreground .upload-marquee-layout .upload-marquee-right .media-container video { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + line-height: 0; +} + +/* ===== RESPONSIVE ===== */ +@media screen and (min-width: 600px) { + .upload-marquee-block .upload-marquee-right .media-container { + border-radius: var(--um-radius-md); + } + + .upload-marquee-block .upload-marquee-left { + margin: 48px var(--grid-margins-width) 0; + } + + .upload-marquee-block .upload-marquee-uploads > .tablet-up, + .upload-marquee-block .upload-marquee-uploads > .tablet-up.desktop-up, + .upload-marquee-block .upload-marquee-media > .tablet-up, + .upload-marquee-block .upload-marquee-media > .tablet-up.desktop-up { + display: block; + } + + .upload-marquee-block .upload-marquee-uploads > .mobile-up, + .upload-marquee-block .upload-marquee-uploads > .desktop-up, + .upload-marquee-block .upload-marquee-media > .mobile-up, + .upload-marquee-block .upload-marquee-media > .desktop-up { + display: none; + } +} + +@media screen and (min-width: 1200px) { + .upload-marquee-block .upload-marquee-layout .upload-marquee-left .upload-marquee-content h1 { + font-size: 64px; + letter-spacing: -2.88px; + } + + .upload-marquee-block .upload-marquee-left { + margin: 0; + padding: 56px 137px; + box-sizing: border-box; + } + + .upload-marquee-block .upload-marquee-layout { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + min-height: 700px; + overflow: hidden; + box-shadow: var(--um-shadow-layout); + } + + .upload-marquee-block .upload-marquee-right { + min-height: auto; + height: 100%; + align-self: stretch; + align-items: stretch; + padding: 24px 24px 24px 0; + } + + .upload-marquee-block .upload-marquee-media { + height: 100%; + } + + .upload-marquee-block .upload-marquee-right .media-container { + min-height: 100%; + max-height: 100%; + } + + .upload-marquee-block .upload-marquee-uploads > .desktop-up, + .upload-marquee-block .upload-marquee-uploads > .tablet-up.desktop-up, + .upload-marquee-block .upload-marquee-media > .desktop-up, + .upload-marquee-block .upload-marquee-media > .tablet-up.desktop-up { + display: block; + } + + .upload-marquee-block .upload-marquee-uploads > .mobile-up, + .upload-marquee-block .upload-marquee-uploads > .tablet-up, + .upload-marquee-block .upload-marquee-media > .mobile-up, + .upload-marquee-block .upload-marquee-media > .tablet-up { + display: none; + } + + .upload-marquee-block .upload-marquee-right .media-container .video-container .pause-play-wrapper { + bottom: 2%; + right: 2%; + left: auto; + top: auto; + } +} + +/* ===== REDUCED MOTION ===== */ +@media (prefers-reduced-motion: reduce) { + .upload-marquee-block .upload-marquee-right .media-container .video-container .pause-play-wrapper { + display: none; + } +} diff --git a/creativecloud/blocks/upload-marquee/upload-marquee.js b/creativecloud/blocks/upload-marquee/upload-marquee.js new file mode 100644 index 000000000..160b8512d --- /dev/null +++ b/creativecloud/blocks/upload-marquee/upload-marquee.js @@ -0,0 +1,480 @@ +import { createTag, getConfig, getLibs } from '../../scripts/utils.js'; + +// ===== CONFIG ===== +const miloLibs = getLibs('/libs'); +const VIEWPORTS = ['mobile-up', 'tablet-up', 'desktop-up']; +const DEFAULT_DROPZONE_ICON = '/cc-shared/assets/svg/s2-icon-upload-20-n.svg'; +const ARIA_PLACEHOLDER_KEYS = { + dropZoneAriaLabel: 'upload-marquee-drop-zone-aria-label', + layoutAriaLabel: 'upload-marquee-layout-aria-label', + filePickerAriaSuffix: 'file-picker', +}; +const ARIA_LABEL_DEFAULTS = { + dropZoneAriaLabel: + 'Upload your asset. Drag and drop a file, or press Enter to browse.', + layoutAriaLabel: + 'Asset upload area. Drag and drop files anywhere in this section.', + filePickerAriaSuffix: 'file picker', +}; +const AnalyticsKeys = { + uploadAssetCTA: 'Upload asset CTA|UnityWidget', + editPhotosCTA: 'Edit Photos CTA|UnityWidget', +}; + +let uploadColumnCounter = 0; + +// ===== LOGGING ===== +function logUploadMarqueeInfo(message, errorType = 'i') { + window.lana?.log(message, { tags: 'upload-marquee', errorType }); +} + +// ===== ID HELPERS ===== +function nextUploadColumnId() { + uploadColumnCounter += 1; + return uploadColumnCounter; +} + +function buildScopedId(prefix, columnId) { + return `${prefix}-${columnId}`; +} + +// ===== ARIA / LOCALIZATION ===== +function createAriaLabelsLoader() { + let labelsPromise; + return async function getAriaLabels() { + if (!labelsPromise) { + labelsPromise = (async () => { + try { + const config = getConfig(); + const { replaceKeyArray } = await import( + `${miloLibs}/features/placeholders.js` + ); + const labelKeys = Object.keys(ARIA_PLACEHOLDER_KEYS); + const localizedValues = await replaceKeyArray( + labelKeys.map((key) => ARIA_PLACEHOLDER_KEYS[key]), + config, + ); + return labelKeys.reduce((acc, key, index) => { + acc[key] = localizedValues[index] || ARIA_LABEL_DEFAULTS[key]; + return acc; + }, {}); + } catch (err) { + logUploadMarqueeInfo( + `Failed to fetch upload marquee aria labels: ${err}`, + ); + return { ...ARIA_LABEL_DEFAULTS }; + } + })(); + } + return labelsPromise; + }; +} + +// ===== DATA EXTRACTION ===== +function extractUploadContentParts(content) { + const media = content.querySelector('picture, .video-container.video-holder'); + const terms = content.querySelector('p:last-child'); + const mediaPara = media?.closest('p'); + + const hasUploadMarker = (para) => para.querySelector( + 'span[class*=icon-share], span[class*=icon-upload], img[src$=".svg"]:not(.video-container img)', + ); + + const candidateParagraphs = [ + ...content.querySelectorAll('p:not(:last-child)'), + ].filter( + (para) => para.textContent.trim() !== '' || para.querySelector('img, svg'), + ); + + const uploadPara = candidateParagraphs.find((para) => hasUploadMarker(para)); + + const contentParagraphs = candidateParagraphs.filter((para) => { + const isMediaOnlyPara = para === mediaPara + && !hasUploadMarker(para) + && para.textContent.trim() === ''; + return !isMediaOnlyPara; + }); + + const textParas = contentParagraphs.filter((para) => para !== uploadPara); + const headingPara = textParas[0]; + const bodyPara = textParas[1]; + + return { + media, + terms, + contentParagraphs, + uploadPara, + headingPara, + bodyPara, + }; +} + +// ===== DOM BUILDERS ===== +// TODO: See if it could be simplified +function applyViewportClasses(foreground) { + foreground.firstElementChild?.classList.add('upload-grid'); + + if ( + foreground.childElementCount === 2 + || foreground.childElementCount === 3 + ) { + [...foreground.children].forEach((child, index) => { + child.classList.add('upload-grid', VIEWPORTS[index]); + if (foreground.childElementCount === 2 && index === 1) { + child.className = 'upload-grid tablet-up desktop-up'; + } + }); + } else if (foreground.childElementCount === 1) { + foreground.firstElementChild?.classList.add(...VIEWPORTS); + } + + return foreground; +} + +function buildDropZoneIcon() { + const defaultIcon = createTag('p', { class: 'drop-zone-default-icon' }); + const image = createTag('img', { src: DEFAULT_DROPZONE_ICON, alt: '' }); + defaultIcon.setAttribute('aria-hidden', 'true'); + defaultIcon.append(image); + return defaultIcon; +} + +function assignDropZoneTextIds(headingPara, bodyPara, columnId) { + const describedByIds = []; + + if (headingPara) { + headingPara.classList.add('drop-zone-heading'); + headingPara.id = buildScopedId('drop-zone-heading', columnId); + describedByIds.push(headingPara.id); + } + if (bodyPara) { + bodyPara.classList.add('drop-zone-body'); + bodyPara.id = buildScopedId('drop-zone-body', columnId); + describedByIds.push(bodyPara.id); + } + + return describedByIds; +} + +async function buildUploadActionControls(para, columnId, getAriaLabels) { + const buttonLabel = para.textContent.trim().split('|')[0].trim() || 'Upload your image'; + const { filePickerAriaSuffix } = await getAriaLabels(); + const button = createTag( + 'button', + { + type: 'button', + class: 'con-button blue action-button button-xl no-track', + 'daa-ll': AnalyticsKeys.uploadAssetCTA, + 'aria-label': buttonLabel, + }, + para.innerHTML, + ); + const input = createTag('input', { + type: 'file', + name: 'file-upload', + id: 'file-upload', + class: 'file-upload hide', + accept: 'image/*', + 'aria-label': `${buttonLabel} ${filePickerAriaSuffix}`, + }); + + para.classList.add('upload-action-container'); + para.textContent = ''; + para.append(button, input); + return { fileInput: input }; +} + +// ===== EVENT BINDERS ===== +function wireDropZoneAccessibility( + dropZone, + fileInput, + describedByIds, + dropZoneAriaLabel, +) { + dropZone.setAttribute('role', 'button'); + dropZone.setAttribute('tabindex', '0'); + dropZone.setAttribute('aria-label', dropZoneAriaLabel); + + if (fileInput?.id) { + dropZone.setAttribute('aria-controls', fileInput.id); + } + if (describedByIds.length) { + dropZone.setAttribute('aria-describedby', describedByIds.join(' ')); + } + + dropZone.addEventListener('click', (event) => { + event.stopPropagation(); + fileInput?.click(); + }); + + dropZone.addEventListener('keydown', (event) => { + if (event.target !== dropZone) return; + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + fileInput?.click(); + } + }); +} + +async function buildDropZone(uploadParts, columnId, getAriaLabels) { + const { dropZoneAriaLabel } = await getAriaLabels(); + const dropZone = createTag('div', { class: 'drop-zone' }); + const { fileInput } = await buildUploadActionControls( + uploadParts.uploadPara, + columnId, + getAriaLabels, + ); + const describedByIds = assignDropZoneTextIds( + uploadParts.headingPara, + uploadParts.bodyPara, + columnId, + ); + + wireDropZoneAccessibility( + dropZone, + fileInput, + describedByIds, + dropZoneAriaLabel, + ); + dropZone.append(buildDropZoneIcon(), ...uploadParts.contentParagraphs); + + return dropZone; +} + +function replaceUploadColumnContent( + content, + mediaContainer, + dropZoneContainer, + terms, +) { + content.textContent = ''; + content.append(mediaContainer, dropZoneContainer); + if (terms) { + dropZoneContainer.append(terms); + } +} + +function buildMarqueeContent(marqueeCell) { + const marqueeContent = createTag('div', { class: 'upload-marquee-content' }); + [...marqueeCell.children].forEach((child) => marqueeContent.append(child.cloneNode(true))); + + const brandingPara = marqueeContent.querySelector(':scope > p:first-child'); + const [firstPicture, secondPicture] = brandingPara + ? [...brandingPara.querySelectorAll('picture')] + : []; + if (firstPicture && secondPicture) { + const brandingRow = createTag('span', { class: 'upload-marquee-branding-row' }); + const firstWrap = createTag('span', { class: 'upload-marquee-branding-first' }); + const secondWrap = createTag('span', { class: 'upload-marquee-branding-second' }); + firstWrap.append(firstPicture.cloneNode(true)); + secondWrap.append(secondPicture.cloneNode(true)); + brandingRow.append(firstWrap, secondWrap); + + brandingPara.textContent = ''; + brandingPara.classList.add('upload-marquee-branding'); + brandingPara.append(brandingRow); + } + + const ctaLink = marqueeContent.querySelector( + 'p strong a[href], p:last-of-type a[href]', + ); + if (ctaLink) { + ctaLink.classList.add('con-button', 'upload-marquee-cta', 'no-track'); + ctaLink.setAttribute('aria-label', ctaLink.textContent.trim()); + ctaLink.setAttribute('daa-ll', AnalyticsKeys.editPhotosCTA); + } + + return marqueeContent; +} + +function buildLayout(layoutAriaLabel) { + const layout = createTag('div', { + class: 'upload-marquee-layout', + role: 'region', + 'aria-label': layoutAriaLabel, + }); + const leftCol = createTag('div', { class: 'upload-marquee-left' }); + const rightCol = createTag('div', { class: 'upload-marquee-right' }); + const uploadsWrapper = createTag('div', { class: 'upload-marquee-uploads' }); + const mediaWrapper = createTag('div', { class: 'upload-marquee-media' }); + + return { + layout, + leftCol, + rightCol, + uploadsWrapper, + mediaWrapper, + }; +} + +function appendColumns(viewportContent, uploadsWrapper, mediaWrapper) { + viewportContent.forEach(({ media, dropZone, viewportClasses }) => { + if (dropZone) { + dropZone.classList.add(...viewportClasses); + uploadsWrapper.append(dropZone); + } + if (media) { + media.classList.add(...viewportClasses); + mediaWrapper.append(media); + } + }); +} + +function collectViewportContent(uploadRow) { + return [...uploadRow.children].map((content) => { + const media = content.querySelector(':scope > .media-container'); + const dropZone = content.querySelector(':scope > .drop-zone-container'); + const viewportClasses = [...content.classList].filter((cls) => VIEWPORTS.includes(cls)); + return { media, dropZone, viewportClasses }; + }); +} + +async function decorateUploadColumn(content, getAriaLabels) { + const columnId = nextUploadColumnId(); + const mediaContainer = createTag('div', { class: 'media-container' }); + const dropZoneContainer = createTag('div', { class: 'drop-zone-container' }); + const uploadParts = extractUploadContentParts(content); + + if (uploadParts.media) { + mediaContainer.append(uploadParts.media); + if ( + uploadParts.media.parentElement?.tagName === 'P' + && uploadParts.media.parentElement.textContent.trim() === '' + ) { + uploadParts.media.parentElement.remove(); + } + } + + if (!uploadParts.uploadPara) { + logUploadMarqueeInfo( + 'Failed to create upload button for upload-marquee block.', + ); + return; + } + + const dropZone = await buildDropZone(uploadParts, columnId, getAriaLabels); + dropZoneContainer.append(dropZone); + replaceUploadColumnContent( + content, + mediaContainer, + dropZoneContainer, + uploadParts.terms, + ); +} + +function setupLayoutDragAndDrop(layout, uploadsWrapper) { + let activeDropZone; + + const setActiveDropZone = () => { + const dropZones = [ + ...uploadsWrapper.querySelectorAll( + ':scope > .drop-zone-container > .drop-zone', + ), + ]; + const nextDropZone = dropZones.find((zone) => zone.offsetParent !== null) || dropZones[0]; + if (activeDropZone && activeDropZone !== nextDropZone) { + activeDropZone.classList.remove('active'); + } + activeDropZone = nextDropZone; + activeDropZone?.classList.add('active'); + }; + + const clearActiveDropZone = () => { + activeDropZone?.classList.remove('active'); + activeDropZone = null; + }; + + layout.addEventListener('dragenter', (event) => { + event.preventDefault(); + setActiveDropZone(); + }); + + layout.addEventListener('dragover', (event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + setActiveDropZone(); + }); + + layout.addEventListener('dragleave', (event) => { + event.preventDefault(); + clearActiveDropZone(); + }); + + document.addEventListener('dragend', () => clearActiveDropZone()); + + layout.addEventListener('drop', (event) => { + event.preventDefault(); + setActiveDropZone(); + const fileInput = activeDropZone?.querySelector('.file-upload'); + const files = event.dataTransfer?.files; + if (files?.length && fileInput) { + try { + fileInput.files = files; + } catch { + // TODO: Trigger lana log + // Some browsers may not allow assigning FileList directly. + } + fileInput.dispatchEvent(new Event('change', { bubbles: true })); + } + clearActiveDropZone(); + }); + + uploadsWrapper.querySelectorAll('.drop-zone').forEach((zone) => { + zone.addEventListener('drop', () => { + clearActiveDropZone(); + }); + }); + + window.addEventListener('drop', () => clearActiveDropZone()); + window.addEventListener('dragend', () => clearActiveDropZone()); +} + +export default async function init(el) { + const { decorateBlockBg } = await import(`${miloLibs}/utils/decorate.js`); + const getAriaLabels = createAriaLabelsLoader(); + + el.classList.add('upload-marquee-block', 'con-block'); + const rows = el.querySelectorAll(':scope > div'); + if (rows.length < 3) return; + + const [backgroundRow, marqueeRow, uploadRow] = rows; + + if (backgroundRow.textContent.trim() !== '') { + backgroundRow.classList.add('background'); + decorateBlockBg(el, backgroundRow, { useHandleFocalpoint: true }); + } + + uploadRow.classList.add('foreground'); + applyViewportClasses(uploadRow); + + for (let i = 0; i < uploadRow.children.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await decorateUploadColumn(uploadRow.children[i], getAriaLabels); + } + + const { layoutAriaLabel } = await getAriaLabels(); + const { layout, leftCol, rightCol, uploadsWrapper, mediaWrapper } = buildLayout(layoutAriaLabel); + + const marqueeCell = marqueeRow.querySelector(':scope > div'); + if (!marqueeCell) return; + + leftCol.append(buildMarqueeContent(marqueeCell)); + appendColumns( + collectViewportContent(uploadRow), + uploadsWrapper, + mediaWrapper, + ); + + if (!uploadsWrapper.children.length || !mediaWrapper.children.length) return; + + leftCol.append(uploadsWrapper); + rightCol.append(mediaWrapper); + layout.append(leftCol, rightCol); + setupLayoutDragAndDrop(layout, uploadsWrapper); + + const foreground = createTag('div', { class: 'foreground' }); + foreground.append(layout); + + el.textContent = ''; + el.append(foreground); +} diff --git a/creativecloud/features/parallax.js b/creativecloud/features/parallax.js index faff46f7e..becd75bd7 100644 --- a/creativecloud/features/parallax.js +++ b/creativecloud/features/parallax.js @@ -24,6 +24,9 @@ function throttle(cb, delay, { trailing = false } = {}) { function addProgressIMPL(el, NAV_HEIGHT, markers) { let screenHeight = window.innerHeight; let elHeight = el.offsetHeight; + let previousButtonTop = 0; + const content = el.querySelector('.firefly-model-showcase-content'); + const initialContentHeight = content.clientHeight; window.addEventListener( 'resize', throttle(() => { @@ -60,10 +63,18 @@ function addProgressIMPL(el, NAV_HEIGHT, markers) { window.addEventListener( 'scroll', () => { + /* if content height changed due to additional spacing (e.g. dylan text spacing), + skip the parallax animation */ + if (initialContentHeight !== content.clientHeight) return; + const buttonRect = el.querySelector('.firefly-model-showcase-content .action-area a:first-of-type').getBoundingClientRect(); + const currentButtonTop = buttonRect.top; + // if the button is below the fold, skip the parallax animation + if (previousButtonTop - screenHeight > 0 && !(previousButtonTop < 0)) return; if (!ticking) { requestAnimationFrame(updateProgress); ticking = true; } + previousButtonTop = currentButtonTop; }, { passive: true }, ); diff --git a/creativecloud/scripts/utils.js b/creativecloud/scripts/utils.js index 79863e938..addf4a66a 100644 --- a/creativecloud/scripts/utils.js +++ b/creativecloud/scripts/utils.js @@ -190,7 +190,7 @@ export const [setLibs, getLibs] = (() => { const miloLibs = setLibs('/libs'); // eslint-disable-next-line object-curly-newline -const { createTag, localizeLink, getConfig, loadStyle, loadLink, loadScript, createIntersectionObserver } = await import(`${miloLibs}/utils/utils.js`); +const { createTag, localizeLink, getConfig, getMetadata, loadStyle, loadLink, loadScript, createIntersectionObserver } = await import(`${miloLibs}/utils/utils.js`); // eslint-disable-next-line max-len export { createTag, loadStyle, loadLink, loadScript, localizeLink, createIntersectionObserver, getConfig }; @@ -451,6 +451,7 @@ export const scriptInit = async () => { decorateArea(); (function loadStyles() { const paths = [`${miloLibs}/styles/styles.css`]; + if (getMetadata('theme') === 'doodlebug') paths.push('/creativecloud/styles/doodlebug.css'); paths.forEach((path) => { const link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); @@ -458,6 +459,35 @@ export const scriptInit = async () => { document.head.appendChild(link); }); }()); + + // TODO can be removed after 2026-03-20 - this is a temporary test for lingo + (function setupInteractionLogging() { + const cutoff = new Date(2026, 2, 20, 23, 59, 59, 999); + const isWithinLoggingWindow = () => new Date() <= cutoff; + + const opts = { capture: true }; + let logged = false; + const logInteraction = (e) => { + if (!e.target?.closest('a')) return; + if (e.type === 'keydown') { + if (e.key !== 'Enter') return; + } + if (!isWithinLoggingWindow() || logged) return; + logged = true; + document.removeEventListener('click', logInteraction, opts); + document.removeEventListener('keydown', logInteraction, opts); + document.removeEventListener('touchstart', logInteraction, opts); + const firstSection = document.querySelector('main > .section'); + const inFirstSection = firstSection?.contains(e.target); + const tag = inFirstSection ? 'test-lingo-user-interaction-first-section' : 'test-lingo-user-interaction-other-section'; + const timeToInteractionMs = Math.round(performance.now()); + window.lana?.log(`${timeToInteractionMs}`, { sampleRate: 1, severity: 'i', tags: tag }); + }; + document.addEventListener('click', logInteraction, opts); + document.addEventListener('keydown', logInteraction, opts); + document.addEventListener('touchstart', logInteraction, opts); + }()); + (async function loadPage() { loadLana({ clientId: 'cc' }); await loadArea(); diff --git a/creativecloud/styles/doodlebug.css b/creativecloud/styles/doodlebug.css new file mode 100644 index 000000000..eda3e7c23 --- /dev/null +++ b/creativecloud/styles/doodlebug.css @@ -0,0 +1,534 @@ +:root:has(meta[name='theme'][content='doodlebug']) { + /* ---- Common Variables ---- */ + --xl-rounded-corners: 20px; + --glass-border: 3px solid rgba(255 255 255 / 30%); + --content-margin: 6.51%; + + @media (min-width: 600px) { + --xl-rounded-corners: 32px; + } + + :where(.section:not(.dark) p) { + color: #5D5D5D; + } + + /* ---- Accordion ---- */ + .accordion { + margin: 0 var(--content-margin); + } + + .accordion .descr-term button { + border-radius: 16px; + background: #F0F0F7; + border: 1px solid #fff; + } + + .descr-list.accordion { + border-bottom: none; + max-width: none; + } + + .accordion .descr-term button[aria-expanded="true"]::before { + content: none; + } + + /* ---- Aside ---- */ + .aside .foreground.container { + display: grid; + align-items: center; + gap: 165px; + width: 85%; + grid-template-columns: minmax(45%, 1fr) minmax(55%, 1fr); + } + + .aside .foreground.container:has(> .image:first-child) { + grid-template-columns: minmax(55%, 1fr) minmax(45%, 1fr); + } + + .aside .foreground.container .text { + margin-inline: 180px 0; + } + + .aside .foreground.container:has(> .image:first-child) .text { + margin-inline: 0 180px; + } + + .aside .foreground.container .image img { + border-radius: 20px; + border: var(--glass-border); + } + + @media (max-width: 1200px) { + .aside .foreground.container { + display: grid; + align-items: center; + gap: 8.5%; + width: 85%; + grid-template-columns: minmax(45%, 1fr) minmax(55%, 1fr); + } + + .aside .foreground.container .text { + margin-inline: 9.5% 0; + } + + .aside .foreground.container:has(> .image:first-child) .text { + margin-inline: 0 9.5%; + } + } + + /* ---- Carousel (Tablet Hinting) ---- */ + .carousel.hinting-tablet { + --tablet-card-width: calc(300 / 768 * 100vw); + --tablet-gap: 24px; + --tablet-margin: 2rem; + } + + .carousel.hinting-mobile .carousel-button-container { + background: none; + min-width: 100px; + gap: 6px; + } + + .carousel.hinting-mobile .carousel-button-container .carousel-controls { + display: none; + } + + .carousel.hinting-mobile .carousel-button-container .carousel-button { + width: 40px; + height: 40px; + background-color: #242424; + } + + .carousel.hinting-mobile .carousel-previous path, + .carousel.hinting-mobile .carousel-next path { + fill: #fff; + } + + .carousel.disable-circular-nav .carousel-button-container .disabled { + background-color: rgba(177 177 177 / 20%); + } + + .carousel.disable-circular-nav .carousel-button-container .disabled path { + fill: #B1B1B1; + } + + .carousel.hinting-mobile:has(.carousel-slide:nth-last-child(1).active) .carousel-slide:first-child { + visibility: hidden; + } + + @media (min-width: 600px) and (max-width: 1199px) { + .carousel.hinting-tablet { + --tablet-card-width: calc(300 / 768 * 100vw); + --tablet-gap: 24px; + --tablet-margin: 2rem; + } + + .carousel.hinting-tablet .carousel-button-container { + background: none; + min-width: 100px; + gap: 6px; + } + + .carousel.hinting-tablet .carousel-button-container .carousel-controls { + display: none; + } + + .carousel.hinting-tablet .carousel-button-container .carousel-button { + width: 40px; + height: 40px; + background-color: #242424; + } + + .carousel.hinting-tablet .carousel-previous path, + .carousel.hinting-tablet .carousel-next path { + fill: #fff; + } + + .carousel.container.hinting-tablet { + width: 100%; + } + + .carousel.hinting-tablet .carousel-wrapper { + margin-left: var(--grid-margins-width); + } + + .carousel.hinting-tablet .carousel-slides { + gap: var(--tablet-gap); + left: calc(-1 * var(--tablet-card-width)); + transform: translateX(var(--tablet-card-width)); + align-items: flex-start; + } + + .carousel.hinting-tablet .carousel-slides.is-reversing { + transform: translateX(calc(-1 * var(--tablet-card-width))); + } + + .carousel.hinting-tablet .carousel-slide { + flex-basis: var(--tablet-card-width); + } + + .carousel.hinting-tablet .carousel-slides.is-ready, + .carousel.hinting-tablet .carousel-slides.is-ready.is-reversing { + transform: translateX(calc(-1 * var(--tablet-gap))); + } + + .carousel.disable-circular-nav .carousel-button-container .disabled { + background-color: rgba(177 177 177 / 20%); + } + + .carousel.disable-circular-nav .carousel-button-container .disabled path { + fill: #B1B1B1; + } + + .carousel.hinting-tablet:has(.carousel-slide:nth-last-child(2).active) .carousel-slide:first-child { + visibility: hidden; + } + + .carousel.disable-nav.hinting-tablet .carousel-slides { + left: calc(-1 * var(--tablet-card-width) - var(--tablet-margin)); + transform: translateX(calc(var(--tablet-card-width) + var(--tablet-margin))); + align-items: flex-start; + } + + .carousel.disable-nav.hinting-tablet .carousel-slides.is-reversing { + transform: translateX(calc(-1 * var(--tablet-card-width) - var(--tablet-margin))); + } + + .carousel.disable-nav.hinting-tablet .carousel-slides.is-ready, + .carousel.disable-nav.hinting-tablet .carousel-slides.is-ready.is-reversing { + transform: translateX(calc(var(--tablet-margin) - var(--tablet-gap))); + } + + .carousel.hinting-tablet .editorial-card { + --card-height-default: calc(var(--tablet-card-width) * 225 / 300); + } + + .carousel.disable-nav.hinting-tablet .editorial-card .media-area::after { + position: absolute; + content: ''; + inset: 0; + border: var(--glass-border); + pointer-events: none; + } + } + + /* ---- Editorial Card ---- */ + .editorial-card.l-shadow { + box-shadow: 0 5px 20px 10px #0000000F; + } + + .editorial-card h3 { + letter-spacing: -3%; + font-weight: 800; + + } + + .editorial-card.s-padding-media .media-area>div { + padding: var(--spacing-xxs); + } + + .editorial-card.s-padding-media.m-rounded-corners-image .media-area>div img { + border-radius: var(--m-rounded-corners); + } + + .two-up .editorial-card .media-area :is(img, video), + .three-up .editorial-card .media-area :is(img, video), + .four-up .editorial-card .media-area :is(img, video) { + height: auto; + max-height: unset; + aspect-ratio: 284 / 269; + } + + .editorial-card .foreground .body-l { + font-size: 1rem; + line-height: 135%; + letter-spacing: 1%; + } + + .section.show-4-desktop { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(276px, 1fr)); + gap: var(--spacing-s); + align-items: start; + padding-left: var(--grid-margins-width); + padding-right: var(--grid-margins-width); + justify-content: center; + } + + + @media (min-width: 1200px) { + .editorial-card .foreground .body-l { + font-size: 1.25rem; + letter-spacing: 1%; + } + + .section.show-2-desktop { + grid-template-columns: repeat(2, minmax(0, 823px)); + max-width: 100%; + padding-left: var(--content-margin); + padding-right: var(--content-margin); + justify-content: center; + } + + .section.show-4-desktop { + grid-template-columns: repeat(4, minmax(0, 350px)); + max-width: 100%; + padding-left: var(--content-margin); + padding-right: var(--content-margin); + gap: 10px; + } + + .show-2-desktop .editorial-card .media-area :is(img, video) { + height: auto; + max-height: unset; + aspect-ratio: 410/336; + } + + .show-2-desktop .editorial-card { + background: none !important; + box-shadow: none; + } + + .show-2-desktop .editorial-card .media-area, + .show-4-desktop .editorial-card .media-area { + position: relative; + } + + .show-2-desktop .editorial-card .media-area::after { + position: absolute; + content: ''; + inset: 0; + border: var(--glass-border); + pointer-events: none; + } + + .show-2-desktop .editorial-card .media-area, + .show-2-desktop .editorial-card .media-area::after { + border-radius: 20px; + } + + .show-4-desktop .editorial-card .foreground > div { + row-gap: 24px; + } + + .show-2-desktop .editorial-card.s-padding-media .media-area>div { + padding: 0; + } + + .show-4-desktop .editorial-card .foreground .body-l { + line-height: 26px; + letter-spacing: 0.2px; /* 130% */ + } + } + + /* ---- Section Utilities ---- */ + .section.two-up, + .section.three-up, + .section.four-up, + .section.five-up { + gap: var(--spacing-s); + } + + .section.spacing-bottom-180 { + padding-bottom: 80px; + } + + .section.xl-top-rounded-corners { + border-radius: var(--xl-rounded-corners) var(--xl-rounded-corners) 0 0; + } + + .section.xl-bottom-rounded-corners { + border-radius: 0 0 var(--xl-rounded-corners) var(--xl-rounded-corners); + } + + + + @media (max-width: 599px) { + .section.no-spacing-bottom-mobile { + padding-bottom: 0; + } + } + + @media (min-width: 600px) { + .section.spacing-bottom-180 { + padding-bottom: 160px; + } + } + + @media (min-width: 600px) and (max-width: 1199px) { + .section.no-spacing-bottom-tablet { + padding-bottom: 0; + } + + .section.s-spacing:has(> .carousel.show-2.ups-desktop) { + padding-top: 0; + } + } + + /* ---- Text Block ---- */ + .text-block [class*="heading-"] { + font-weight: 900; + } + + /* ---- Aside (Heading Override) ---- */ + .aside .foreground.container .text [class*="heading-"] { + font-size: 2.25rem; + font-weight: 800; + letter-spacing: -3%; + line-height: 98%; + } + + .section:not(.dark) .text-block:not(.dark) [class*="heading-"] { + color: var(--color-black); + } + + .text-block [class*="heading-xxxl"] { + font-size: 2rem; + line-height: 105%; + letter-spacing: -1.44px; + } + + .text-block.center [class*="body-l"] { + font-size: 1.125rem; + line-height: 130%; + } + + .text-block.grid-cta .foreground { + --cell-width: 300px; + + max-width: 100%; + margin: 0 var(--content-margin); + } + + .text-block.grid-cta .foreground .cta-container .action-area { + display: grid; + grid-template-columns: 1fr; + justify-content: center; + gap: 10px; + max-width: 100%; + } + + .text-block.grid-cta .foreground .cta-container .action-area .con-button { + display: block; + position: relative; + width: 100%; + height: 81px; + padding: 24px 48px 24px 18px; + border: none; + border-radius: 8px; + background: var(--color-white); + color: var(--text-color); + font-size: 1.25rem; + font-weight: 800; + line-height: 33px; + box-sizing: border-box; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .text-block.grid-cta .foreground .cta-container .action-area .con-button:hover, + .text-block.grid-cta .foreground .cta-container .action-area .con-button:focus-visible { + color: var(--color-black); + position: relative; + z-index: 1; + box-shadow: 0 1px 4px 0 #0000000C, 0 8px 8px 0 #00000013, 0 20px 12px 0 #00000018, 0 40px 24px 0 #0000001D; + } + + /** TODO: rtl **/ + + .text-block.grid-cta .foreground .cta-container a::after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + right: 18px; + top: 50%; + transform: translateY(-50%); + background: url('/cc-shared/assets/svg/s2-icon-openin-20-n.svg') no-repeat; + background-size: contain; + } + + @media (min-width: 600px) { + .text-block.grid-cta .foreground .cta-container .action-area { + grid-template-columns: repeat(2,1fr); + } + + + .text-block.grid-cta .foreground .cta-container .action-area .con-button { + font-size: 1.5rem; + letter-spacing: -3%; + } + } + + @media (min-width: 600px) and (max-width: 1199px) { + .text-block.left-tablet { + text-align: start; + } + + .text-block.left-tablet .action-area { + justify-content: start; + } + + .text-block [class*="heading-xxxl"] { + font-size: 2.25rem; + line-height: 98%; + letter-spacing: -1.62px; + } + + .text-block.grid-cta .foreground { + --cell-width: 310px; + } + } + + @media (min-width: 1200px) { + .text-block [class*="heading-xxxl"] { + font-size: 4rem; + line-height: 98%; + letter-spacing: -2.88px; + } + + .text-block.center [class*="body-l"] { + font-size: 1.25rem; + line-height: 130%; + max-width: 950px; + margin: 0 auto; + } + + .text-block.grid-cta .foreground { + --cell-width: 410px; + } + + .text-block.grid-cta .foreground .cta-container .action-area { + grid-template-columns: repeat(4, 1fr); + } + } +} + +/** +TODO: Can use &[dir="rtl"] above also, & is supported by modern browsers, done this for old browsers +**/ + +:root[dir="rtl"]:has(meta[name='theme'][content='doodlebug']) { + .text-block.grid-cta .foreground .cta-container .action-area a.con-button { + text-align: right; + padding: 24px 18px 24px 48px; + } + + .text-block.grid-cta .foreground .cta-container .action-area a.con-button::after { + display: none; + } + + .text-block.grid-cta .foreground .cta-container .action-area a.con-button::before { + content: ''; + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%) scaleX(-1); + width: 20px; + height: 20px; + background: url('/cc-shared/assets/svg/s2-icon-openin-20-n.svg') no-repeat center; + background-size: contain; + } +}