diff --git a/express/code/blocks/floating-panel/floating-panel.css b/express/code/blocks/floating-panel/floating-panel.css new file mode 100644 index 00000000..be8b775c --- /dev/null +++ b/express/code/blocks/floating-panel/floating-panel.css @@ -0,0 +1,66 @@ +.floating-panel { + line-height: 130%; + padding: 16px; + border-radius: 16px; + box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.16); + position: fixed; + z-index: 2; + bottom: 100px; + box-sizing: border-box; + width: 343px; + left: 50%; + transform: translateX(-50%); +} + +.floating-panel.dark { + color: var(--color-white); + background-color: #000b1d; +} + +.floating-panel .header { + font-weight: 700; + font-size: var(--body-font-size-l); + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 8px;; +} +.floating-panel .header .icon{ + width: 10px; + height: 10px; + padding: 7px; +} + +.floating-panel .subheader { + font-weight: 400; + font-size: var(--body-font-size-s); + padding-bottom: 16px; +} + +.floating-panel .link-rows-container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.floating-panel-link-row { + display: flex; + justify-content: space-between; +} + +.floating-panel-link-row div { + display: flex; + gap: 8px; + align-items: center; +} + +.floating-panel-link-row .icon { + width: 22px; + height: 22px; + gap: 4px; +} + +.floating-panel .floating-panel-link-row a.con-button { + /* min-width: 100px; */ + margin: 0; +} diff --git a/express/code/blocks/floating-panel/floating-panel.js b/express/code/blocks/floating-panel/floating-panel.js new file mode 100644 index 00000000..0bf97828 --- /dev/null +++ b/express/code/blocks/floating-panel/floating-panel.js @@ -0,0 +1,28 @@ +import { getLibs, getIconElementDeprecated } from '../../scripts/utils.js'; + +const iconRegex = /icon-\s*([^\s]+)/; +let decorateButtons; let createTag; + +export default async function init(el) { + [{ decorateButtons }, { createTag }] = await Promise.all([import(`${getLibs()}/utils/decorate.js`), import(`${getLibs()}/utils/utils.js`)]); + + decorateButtons(el); + const rows = [...el.children]; + const [header, subheader, ...linkRows] = rows; + header.classList.add('header'); + header.append(getIconElementDeprecated('close-white')); + subheader.classList.add('subheader'); + + const linkRowsContainer = createTag('div', { class: 'link-rows-container' }); + linkRows.forEach((link) => { + link.classList.add('floating-panel-link-row'); + const icon = link.querySelector('.icon'); + const match = icon && iconRegex.exec(icon.className); + if (match?.[1]) { + icon.append(getIconElementDeprecated(match[1])); + } + linkRowsContainer.append(link); + }); + el.append(linkRowsContainer); + return el; +} diff --git a/express/code/blocks/login-page/login-page.css b/express/code/blocks/login-page/login-page.css new file mode 100644 index 00000000..af67fbed --- /dev/null +++ b/express/code/blocks/login-page/login-page.css @@ -0,0 +1,62 @@ +/* make susi-light in the same ax-login-page section hover */ +.section.ax-login-page { + display: flex; + justify-content: center; + align-items: center; +} + +.login-page .background img { + display: none; + width: 100vw; /* not needed if authored right images */ +} + +@media (min-width: 768px) { + .login-page .background .m-background { + display: block; + } +} + +@media (min-width: 1024px) { + .login-page .background .m-background { + display: none; + } + .login-page .background .tablet-background { + display: block; + } +} + +@media (min-width: 1280px) { + .login-page .background .tablet-background { + display: none; + } + .login-page .background .l-background { + display: block; + } +} + +@media (min-width: 1440px) { + .login-page .background .l-background { + display: none; + } + .login-page .background .desktop-background { + display: block; + } +} + +@media (min-width: 1680px) { + .login-page .background .desktop-background { + display: none; + } + .login-page .background .xl-background { + display: block; + } +} + +@media (min-width: 1920px) { + .login-page .background .xl-background { + display: none; + } + .login-page .background .xxl-background { + display: block; + } +} diff --git a/express/code/blocks/login-page/login-page.js b/express/code/blocks/login-page/login-page.js new file mode 100644 index 00000000..6ac238af --- /dev/null +++ b/express/code/blocks/login-page/login-page.js @@ -0,0 +1,10 @@ +// 768/1024/1280/1440/1600/1920 +const breakpoints = ['m', 'tablet', 'l', 'desktop', 'xl', 'xxl']; +export default async function init(el) { + const background = el.children[0]; + background.classList.add('background'); + const imgs = [...background.querySelectorAll('img')]; + imgs.forEach((img, index) => { + img.classList.add(`${breakpoints[index]}-background`); + }); +} diff --git a/express/code/blocks/susi-light/susi-light.css b/express/code/blocks/susi-light/susi-light.css index 649cfff3..54da2064 100644 --- a/express/code/blocks/susi-light/susi-light.css +++ b/express/code/blocks/susi-light/susi-light.css @@ -3,3 +3,143 @@ width: 360px; min-height: 462px; } + +.susi-light { + display: flex; + align-items: center; + justify-content: center; +} + +.susi-tabs { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.susi-tabs .express-logo { + width: unset; + height: 24px; + padding-top: 32px; +} + +.susi-tabs .title { + font-size: var(--heading-font-size-s); + line-height: 28.6px; + font-weight: 900; + padding-top: 12px; + text-align: center; +} + +.susi-tabs [role='tablist'] { + background-color: #e6e6e6; + border-radius: 8px; + display: flex; + gap: 4px; + padding: 4px; + margin-top: 16px; + box-shadow: 0px 2px 8px 0px #00000029; +} + +.susi-tabs [role='tablist']:has(:first-child:last-child) { + display: none; +} + +.susi-tabs [role='tab'] { + background-color: #e6e6e6; + color: #222222; + border: initial; + font-family: var(--body-font-family); + font-size: var(--body-font-size-xs); + font-weight: 700; + padding: 8px 12px 10px; + line-height: 130%; + cursor: pointer; +} + +.susi-tabs [role='tab'][aria-selected='true'] { + background-color: var(--color-white); + border-radius: 6px; +} + +.susi-tabs [role='tabpanel'] { + width: 400px; + max-width: 95vw; +} + +.susi-tabs [role='tabpanel'].hide { + display: none; +} + +/* reduce CLS */ +.susi-tabs [role='tabpanel'].standard .susi-wrapper { + min-height: 457.5px; +} + +.susi-tabs [role='tabpanel'].edu-express .susi-wrapper { + min-height: 366.5px; +} + +.susi-tabs .footer { + font-size: var(--body-font-size-s); + text-align: center; + padding: 16px 0; + font-weight: 700; + color: #292929; + line-height: 20.8px; +} + +.susi-tabs .footer a { + text-decoration: underline; + color: initial; + font-weight: 500; +} + +.susi-tabs .footer.susi-banner { + background-color: #f3f3f3; +} + +.susi-tabs .footer.susi-bubbles h2 { + font-size: var(--body-font-size-l); + font-weight: 700; +} + +.susi-tabs .footer .susi-bubble-container { + display: flex; + gap: 12px; + justify-content: center; + padding-top: 8px; +} + +.susi-tabs .footer .susi-bubble { + font-size: var(--body-font-size-s); + background-color: #f3f3f3; + display: flex; + flex-direction: column; + padding: 12px 24px; + border-radius: 8px; + margin: 0; +} + +/* ax-login-page section metadata styles */ +.section.ax-login-page .susi-tabs { + background-color: var(--color-white); + border-radius: 16px; +} + +.section.ax-login-page .susi-light { + padding-bottom: 32px; +} +.section.ax-login-page .susi-tabs .footer { + border-radius: 8px; +} + +@media (min-width: 768px) { + .section.ax-login-page .susi-light { + position: absolute; + padding-top: 30px; + } + .section.ax-login-page .susi-tabs .footer { + border-radius: 0 0 16px 16px; + } +} diff --git a/express/code/blocks/susi-light/susi-light.js b/express/code/blocks/susi-light/susi-light.js index 9fdfffa4..fc3d4b2e 100644 --- a/express/code/blocks/susi-light/susi-light.js +++ b/express/code/blocks/susi-light/susi-light.js @@ -1,12 +1,14 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable camelcase */ -import { getLibs } from '../../scripts/utils.js'; +import { getLibs, getIconElementDeprecated } from '../../scripts/utils.js'; let createTag; let loadScript; let getConfig; let isStage; let loadIms; -const variant = 'edu-express'; +const DCTX_ID_STAGE = 'v:2,s,dcp-r,bg:express2024,bf31d610-dd5f-11ee-abfd-ebac9468bc58'; +const DCTX_ID_PROD = 'v:2,s,dcp-r,bg:express2024,45faecb0-e687-11ee-a865-f545a8ca5d2c'; + const usp = new URLSearchParams(window.location.search); const onRedirect = (e) => { @@ -21,7 +23,7 @@ const onError = (e) => { window.lana?.log('on error:', e); }; -export function loadWrapper() { +export function loadSUSIScripts() { const CDN_URL = `https://auth-light.identity${isStage ? '-stage' : ''}.adobe.com/sentry/wrapper.js`; return loadScript(CDN_URL); } @@ -40,58 +42,154 @@ function getDestURL(url) { return destURL.toString(); } -export default async function init(el) { - ({ createTag, loadScript, getConfig, loadIms } = await import(`${getLibs()}/utils/utils.js`)); - isStage = (usp.get('env') && usp.get('env') !== 'prod') || getConfig().env.name !== 'prod'; - const rows = el.querySelectorAll(':scope> div > div'); - const redirectUrl = rows[0]?.textContent?.trim().toLowerCase(); - // eslint-disable-next-line camelcase - const client_id = rows[1]?.textContent?.trim() || 'AdobeExpressWeb'; - const title = rows[2]?.textContent?.trim(); - const authParams = { - dt: false, - locale: getConfig().locale.ietf.toLowerCase(), - response_type: 'code', - client_id, - scope: 'AdobeID,openid', - }; - function sendEventToAnalytics(type, eventName) { - const sendEvent = () => { - window._satellite.track('event', { - xdm: {}, - data: { - eventType: 'web.webinteraction.linkClicks', - web: { - webInteraction: { - name: eventName, - linkClicks: { value: 1 }, - type, +function sendEventToAnalytics(type, eventName, client_id) { + const sendEvent = () => { + window._satellite.track('event', { + xdm: {}, + data: { + eventType: 'web.webinteraction.linkClicks', + web: { + webInteraction: { + name: eventName, + linkClicks: { + value: 1, }, + type, }, - /* eslint-disable object-curly-newline */ - _adobe_corpnew: { - digitalData: { - primaryEvent: { - eventInfo: { - eventName, - client_id, - }, + }, + _adobe_corpnew: { + digitalData: { + primaryEvent: { + eventInfo: { + eventName, + client_id, }, }, }, - /* eslint-enable object-curly-newline */ }, - }); - }; - if (window._satellite?.track) { + }, + }); + }; + if (window._satellite?.track) { + sendEvent(); + } else { + window.addEventListener('alloy_sendEvent', () => { sendEvent(); - } else { - window.addEventListener('alloy_sendEvent', () => { - sendEvent(); - }, { once: true }); - } + }, { once: true }); } - const destURL = getDestURL(redirectUrl); +} + +function createSUSIComponent({ variant, config, authParams, destURL }) { + const susi = createTag('susi-sentry-light'); + susi.authParams = authParams; + susi.authParams.redirect_uri = destURL; + susi.authParams.dctx_id = isStage ? DCTX_ID_STAGE : DCTX_ID_PROD; + susi.config = config; + if (isStage) susi.stage = 'true'; + susi.variant = variant; + const onAnalytics = (e) => { + const { type, event } = e.detail; + sendEventToAnalytics(type, event, authParams.client_id); + }; + susi.addEventListener('redirect', onRedirect); + susi.addEventListener('on-error', onError); + susi.addEventListener('on-analytics', onAnalytics); + return susi; +} + +function buildSUSIParams({ client_id, variant, destURL, locale, title, hideIcon }) { + const params = { + variant, + authParams: { + dt: false, + locale, + response_type: 'code', + client_id, + scope: 'AdobeID,openid', + }, + destURL, + config: { + consentProfile: 'free', + fullWidth: true, + }, + }; + if (title !== undefined) { + params.config.title = title; + } + if (hideIcon) { + params.config.hideIcon = true; + } + return params; +} + +function sanitizeId(input) { + return input + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^\w-]/g, ''); +} + +let tabsId = 0; +function buildSUSITabs(el, options) { + tabsId += 1; + const rows = [...el.children]; + const wrapper = createTag('div', { class: 'susi-tabs' }); + const tabList = createTag('div', { role: 'tablist' }); + const susiScriptReady = loadSUSIScripts(); + const panels = options.map((option, i) => { + const { footer, tabName, variant } = option; + const susiWrapper = createTag('div', { class: 'susi-wrapper' }); + const panel = createTag('div', { role: 'tabpanel', class: variant }, susiWrapper); + susiScriptReady.then(() => susiWrapper.append(createSUSIComponent(option))); + + if (footer) { + footer.classList.add('footer'); + if (footer.querySelector('h2')) { + footer.classList.add('susi-bubbles'); + const bubbleContainer = createTag('div', { class: 'susi-bubble-container' }); + [...footer.querySelectorAll('p')].forEach((p) => { + p.classList.add('susi-bubble'); + bubbleContainer.append(p); + }); + footer.append(bubbleContainer); + } else { + footer.classList.add('susi-banner'); + } + panel.append(footer); + } + + const id = sanitizeId(`${tabName}-${tabsId}`); + panel.setAttribute('aria-labelledby', `tab-${id}`); + panel.id = `panel-${id}`; + i > 0 && panel.classList.add('hide'); + const tab = createTag('button', { + role: 'tab', + 'aria-selected': i === 0, + 'aria-controls': `panel-${id}`, + id: `tab-${id}`, + }, tabName); + tab.addEventListener('click', () => { + tabList.querySelector('[aria-selected=true]')?.setAttribute('aria-selected', false); + tab.setAttribute('aria-selected', true); + panels.forEach((p) => { + p !== panel ? p.classList.add('hide') : p.classList.remove('hide'); + }); + }); + tabList.append(tab); + return panel; + }); + + const logo = getIconElementDeprecated('adobe-express-logo'); + logo.classList.add('express-logo'); + logo.height = 24; + const title = rows[0].textContent?.trim(); + const titleDiv = createTag('div', { class: 'title' }, title); + wrapper.append(logo, titleDiv, tabList, ...panels); + return wrapper; +} + +function redirectIfLoggedIn(destURL) { const goDest = () => { sendEventToAnalytics('redirect', 'logged-in-auto-redirect'); window.location.assign(destURL); @@ -106,23 +204,55 @@ export default async function init(el) { }) .catch((e) => { window.lana?.log(`Unable to load IMS in susi-light: ${e}`); }); } - el.innerHTML = ''; - await loadWrapper(); - const config = { consentProfile: 'free' }; - if (title) { config.title = title; } - const susi = createTag('susi-sentry-light'); - susi.authParams = authParams; - susi.authParams.redirect_uri = destURL; - susi.config = config; - if (isStage) susi.stage = 'true'; - susi.variant = variant; +} - const onAnalytics = (e) => { - const { type, event } = e.detail; - sendEventToAnalytics(type, event); - }; - susi.addEventListener('redirect', onRedirect); - susi.addEventListener('on-error', onError); - susi.addEventListener('on-analytics', onAnalytics); - el.append(susi); +export default async function init(el) { + ({ createTag, loadScript, getConfig, loadIms } = await import(`${getLibs()}/utils/utils.js`)); + isStage = (usp.get('env') && usp.get('env') !== 'prod') || getConfig().env.name !== 'prod'; + const locale = getConfig().locale.ietf.toLowerCase(); + const { imsClientId } = getConfig(); + + const isTabs = el.classList.contains('tabs'); + const noRedirect = el.classList.contains('no-redirect'); + + // only edu variant shows single + if (!isTabs) { + const rows = el.querySelectorAll(':scope > div > div'); + const redirectUrl = rows[0]?.textContent?.trim().toLowerCase(); + const client_id = rows[1]?.textContent?.trim() || (imsClientId ?? 'AdobeExpressWeb'); + const title = rows[2]?.textContent?.trim(); + const variant = 'edu-express'; + const params = buildSUSIParams({ + client_id, variant, destURL: getDestURL(redirectUrl), locale, title, + }); + if (!noRedirect) { + redirectIfLoggedIn(params.destURL); + } + await loadSUSIScripts(); + el.replaceChildren(createSUSIComponent(params)); + return; + } + const rows = [...el.children]; + const tabNames = [...rows[1].querySelectorAll('div')].map((div) => div.textContent); + const variants = [...rows[2].querySelectorAll('div')].map((div) => div.textContent?.trim().toLowerCase()); + const redirectUrls = [...rows[3].querySelectorAll('div')].map((div) => div.textContent?.trim().toLowerCase()); + const client_ids = [...rows[4].querySelectorAll('div')].map((div) => div.textContent?.trim() || (imsClientId ?? 'AdobeExpressWeb')); + const footers = rows[5] ? [...rows[5].querySelectorAll('div')] : []; + const tabParams = tabNames.map((tabName, index) => ({ + tabName, + ...buildSUSIParams({ + client_id: client_ids[index], + variant: variants[index], + destURL: getDestURL(redirectUrls[index]), + locale, + title: '', // rm titles + hideIcon: true, + }), + footer: footers[index] ?? null, + })); + if (!noRedirect) { + // redirect to first one if logged in + redirectIfLoggedIn(tabParams[0].destURL); + } + el.replaceChildren(buildSUSITabs(el, tabParams)); } diff --git a/express/code/icons/ax-badge-dark.svg b/express/code/icons/ax-badge-dark.svg new file mode 100644 index 00000000..5ca9c5ff --- /dev/null +++ b/express/code/icons/ax-badge-dark.svg @@ -0,0 +1,25 @@ + diff --git a/express/code/icons/ax-globe.svg b/express/code/icons/ax-globe.svg new file mode 100644 index 00000000..b3d1da31 --- /dev/null +++ b/express/code/icons/ax-globe.svg @@ -0,0 +1,10 @@ + diff --git a/express/code/icons/ax-web-dark.svg b/express/code/icons/ax-web-dark.svg new file mode 100644 index 00000000..7346ef1a --- /dev/null +++ b/express/code/icons/ax-web-dark.svg @@ -0,0 +1,10 @@ + diff --git a/express/code/scripts/express-delayed.js b/express/code/scripts/express-delayed.js index 8e7cc96d..a59afef2 100644 --- a/express/code/scripts/express-delayed.js +++ b/express/code/scripts/express-delayed.js @@ -21,7 +21,7 @@ function preloadSUSILight() { preloadTag.setAttribute('data-stage', 'true'); } import('../blocks/susi-light/susi-light.js') - .then((mod) => mod.loadWrapper()) + .then((mod) => mod.loadSUSIScripts()) .then(() => { document.head.append(preloadTag); }); diff --git a/test/blocks/floating-panel/floating-panel.test.js b/test/blocks/floating-panel/floating-panel.test.js new file mode 100644 index 00000000..db0aa101 --- /dev/null +++ b/test/blocks/floating-panel/floating-panel.test.js @@ -0,0 +1,39 @@ +/* eslint-env mocha */ +/* eslint-disable no-unused-vars */ + +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import { mockRes } from '../test-utilities.js'; + +const locales = { '': { ietf: 'en-US', tk: 'hah7vzn.css' } }; +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +const originalFetch = window.fetch; + +const [{ getLibs }, _, { default: decorate }] = await Promise.all([import('../../../express/code/scripts/utils.js'), import('../../../express/code/scripts/scripts.js'), import( + '../../../express/code/blocks/floating-panel/floating-panel.js' +)]); +await import(`${getLibs()}/utils/utils.js`).then((mod) => { + const conf = { locales }; + mod.setConfig(conf); +}); + +describe('Floating-panel', async () => { + const block = document.querySelector('.floating-panel'); + before(() => { + window.fetch = sinon.stub().callsFake(() => mockRes({})); + }); + + after(() => { + window.fetch = originalFetch; + }); + + it('decorates all required content', async () => { + expect(block).to.exist; + await decorate(block); + expect(block.querySelector('.header')).to.exist; + expect(block.querySelector('.subheader')).to.exist; + expect(block.querySelector('.link-rows-container')).to.exist; + expect(block.querySelectorAll('.floating-panel-link-row').length).to.equal(2); + }); +}); diff --git a/test/blocks/floating-panel/mocks/body.html b/test/blocks/floating-panel/mocks/body.html new file mode 100644 index 00000000..19fb074a --- /dev/null +++ b/test/blocks/floating-panel/mocks/body.html @@ -0,0 +1,18 @@ +
+
Get started in the all-in-one editor
@@ -39,10 +39,10 @@
+
Edit your media in one click
@@ -68,10 +68,10 @@
+
Create fast with generative AI
@@ -89,10 +89,10 @@
+
Explore amazing content
diff --git a/test/blocks/login-page/login-page.test.js b/test/blocks/login-page/login-page.test.js new file mode 100644 index 00000000..9e342717 --- /dev/null +++ b/test/blocks/login-page/login-page.test.js @@ -0,0 +1,24 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; + +const imports = await Promise.all([import('../../../express/code/scripts/utils.js'), import('../../../express/code/scripts/scripts.js')]); +const { getLibs } = imports[0]; +await import(`${getLibs()}/utils/utils.js`).then((mod) => { + const conf = {}; + mod.setConfig(conf); +}); +const [{ default: decorate }] = await Promise.all([import('../../../express/code/blocks/login-page/login-page.js')]); +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +describe('login-page', () => { + let block; + before(async () => { + block = document.querySelector('.login-page'); + decorate(block); + }); + it('has a background image', async () => { + expect(block.querySelector('img.m-background')).to.exist; + expect(block.querySelector('img.l-background')).to.exist; + expect(block.querySelector('img.xl-background')).to.exist; + expect(block.querySelector('img.xxl-background')).to.exist; + }); +}); diff --git a/test/blocks/login-page/mocks/body.html b/test/blocks/login-page/mocks/body.html new file mode 100644 index 00000000..52deeaf8 --- /dev/null +++ b/test/blocks/login-page/mocks/body.html @@ -0,0 +1,79 @@ +