diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..097b5acb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# EDS Release Notes + +Source: [PR #579 — EDS release March 30th, 2026](https://github.com/AdobeDocs/adp-devsite/pull/579) (`stage` → `main`). + +## 3/30/26 EDS Release: + +- GetCredential Fixes: + + - **Fix:** credential design issue + + - **Fix:** sign-in component problem + + - **Fix:** component loading issue + + - **Fix:** populate `templateData.apis` from the template response before credential creation (template install / license configuration) + + - `fetchTemplateEntitlement()` loads the full template, including APIs with `licenseConfigs`, but that data was not always stored on `templateData`, leaving `templateData.apis` undefined. + - The `/install` endpoint could receive an empty `apis` array and fail with errors such as “Service CJA SDK requires selection of a product”. + - Ensures `fetchTemplateEntitlement()` runs in the relevant auth flows so API data is available, and maps only the required `licenseConfig` fields (`id`, `productId`, `op`) in the install request payload. + +- **Fix:** “On this page” styles + +- Data Playground Fixes: + + - **Fix:** code playground metadata and script updates + + - [DEVSITE-2304](https://jira.corp.adobe.com/browse/DEVSITE-2304) + +- **Fix:** dropdown width, selector, and description max width + + - [DEVSITE-2295](https://jira.corp.adobe.com/browse/DEVSITE-2295), [DEVSITE-2305](https://jira.corp.adobe.com/browse/DEVSITE-2305) + +- **Feat:** enforce Node 24+ via `.npmrc` and the `engines` field + + - [DEVSITE-2296](https://jira.corp.adobe.com/browse/DEVSITE-2296) + +- **Fix:** reverse image and text order in columns + + - [DEVSITE-2301](https://jira.corp.adobe.com/browse/DEVSITE-2301), [DEVSITE-2303](https://jira.corp.adobe.com/browse/DEVSITE-2303) + +- **Fix:** rename `B_app_PremierePro.svg` to `premierepro.svg` + +- Embed and media fixes: + + - **Fix:** embed layout and related design issues + + - [DEVSITE-2276](https://jira.corp.adobe.com/browse/DEVSITE-2276), [DEVSITE-2303](https://jira.corp.adobe.com/browse/DEVSITE-2303) + + - **Fix:** video layout (including extra letterboxing) and shorts vs video behavior + + - [DEVSITE-2276](https://jira.corp.adobe.com/browse/DEVSITE-2276), [DEVSITE-2303](https://jira.corp.adobe.com/browse/DEVSITE-2303) + + - **Fix:** remove stray console logging in embed/video paths diff --git a/hlx_statics/blocks/columns/columns.css b/hlx_statics/blocks/columns/columns.css index e639396e..6a0003ce 100644 --- a/hlx_statics/blocks/columns/columns.css +++ b/hlx_statics/blocks/columns/columns.css @@ -28,6 +28,14 @@ main div.columns-wrapper div.columns > div:nth-child(even) { flex-direction: row-reverse; } +main div.columns-wrapper div.columns.isReversed > div:nth-child(odd) { + flex-direction: row-reverse; +} + +main div.columns-wrapper div.columns.isReversed > div:nth-child(even) { + flex-direction: row; +} + main div.columns-wrapper .checkmark ul { list-style: none; font-size: 16px; diff --git a/hlx_statics/blocks/columns/columns.js b/hlx_statics/blocks/columns/columns.js index 121a9865..7b11739e 100644 --- a/hlx_statics/blocks/columns/columns.js +++ b/hlx_statics/blocks/columns/columns.js @@ -33,6 +33,8 @@ function processImages(block) { export default async function decorate(block) { const container = getBlockSectionContainer(block); const variant = block.getAttribute('data-variant') + const isReversed = block.getAttribute('data-isreversed') === 'true'; + isReversed && block.classList.add('isReversed'); const isDocs = IS_DEV_DOCS; isDocs && block.classList.add('isDocs') diff --git a/hlx_statics/blocks/embed/embed.css b/hlx_statics/blocks/embed/embed.css index 126b9a08..f663133e 100644 --- a/hlx_statics/blocks/embed/embed.css +++ b/hlx_statics/blocks/embed/embed.css @@ -2,6 +2,21 @@ main div.embed-wrapper:has(.embed.background-color-white) { background-color: white; } +main div.embed-wrapper .ytShort { + position: relative; + width: 315px; + height: 560px; + aspect-ratio: 9 / 16; + margin: 0 auto; +} + +@media screen and (max-width: 700px) { + main div.embed-wrapper .ytShort { + width: 100%; + max-width: 315px; + } +} + main div.embed-wrapper { align-items: center; justify-content: center; diff --git a/hlx_statics/blocks/embed/embed.js b/hlx_statics/blocks/embed/embed.js index 6cfb7c76..0cc47794 100644 --- a/hlx_statics/blocks/embed/embed.js +++ b/hlx_statics/blocks/embed/embed.js @@ -40,15 +40,15 @@ return embedHTML; const embedYTShort = (url, loop, controls, vidTitle) => { const [, videoCode] = url.pathname.split('/shorts/'); - return ` -
+ return `
+ allowfullscreen + ${vidTitle ? `title="${vidTitle}"` : `title="Content from YouTube"`} + loading="lazy"> +
`; }; @@ -82,7 +82,7 @@ const embedTikTok = (url, loop, controls, vidTitle) => {
`; } -const embedYoutube = (url, loop, controls, vidTitle) => { +const embedYoutube = (url, loop, controls, vidTitle, isShort) => { let vid; const embed = url.pathname; @@ -100,6 +100,19 @@ const embedYoutube = (url, loop, controls, vidTitle) => { if (embed.includes('playlist')) { return embedYTPlaylist(url, loop, controls, vidTitle); } + if (isShort && vid) { + + return `
+ +
`; + } if (vid) { return ` @@ -173,7 +186,8 @@ const loadEmbed = (block, link) => { let controls = 1; const attrs = block?.parentElement?.parentElement?.attributes; const vidTitle = attrs?.getNamedItem('data-videotitle')?.value; - // changes the values of these attributes based on section metadata + const isShort = block.getAttribute('data-short')?.toLowerCase() == 'true'; + // changes the values of these attributes based on section metadata if (attrs?.getNamedItem('data-loop')) { loop = (attrs.getNamedItem('data-loop').value.toLowerCase() === 'true') ? 1: 0; @@ -184,7 +198,7 @@ const loadEmbed = (block, link) => { } const url = new URL(link); if (config) { - block.innerHTML = config.embed(url, loop, controls, vidTitle); + block.innerHTML = config.embed(url, loop, controls, vidTitle, isShort); block.classList.add('block', 'embed', `embed-${config.match[0]}`); } else { block.innerHTML = getDefaultEmbed(url); diff --git a/hlx_statics/blocks/getcredential/getcredential.css b/hlx_statics/blocks/getcredential/getcredential.css index 4799cc33..11c42a03 100644 --- a/hlx_statics/blocks/getcredential/getcredential.css +++ b/hlx_statics/blocks/getcredential/getcredential.css @@ -207,14 +207,12 @@ } .credential-form { - flex: 1; - width: 40%; + width: 100%; max-width: 600px; } .return-left-content, .return-right-content, -.card-content, .project-title-group { flex: 1; } @@ -944,6 +942,7 @@ .card-content { gap: 0; + width: 100%; max-width: 560px; } diff --git a/hlx_statics/blocks/getcredential/getcredential.js b/hlx_statics/blocks/getcredential/getcredential.js index 8a10a953..f09d06d0 100644 --- a/hlx_statics/blocks/getcredential/getcredential.js +++ b/hlx_statics/blocks/getcredential/getcredential.js @@ -193,13 +193,13 @@ async function createCredential() { throw new Error('Template configuration missing'); } - // Prepare APIs data + // Prepare APIs data with licenseConfigs from the template response const apis = templateData.apis?.map(api => ({ code: api.code, credentialType: api.credentialType, flowType: api.flowType, licenseConfigs: Array.isArray(api.licenseConfigs) && api.licenseConfigs.length > 0 - ? [{ ...api.licenseConfigs[0], op: 'add' }] + ? [{ id: api.licenseConfigs[0].id, productId: api.licenseConfigs[0].productId, op: 'add' }] : [], })) || []; @@ -343,6 +343,9 @@ async function fetchTemplateEntitlement() { const response = await fetch(url, { method: 'GET', headers }); if (!response.ok) return null; const data = await response.json(); + if (templateData && Array.isArray(data?.apis)) { + templateData.apis = data.apis; + } const userEntitled = data?.userEntitled !== false; const orgEntitled = data?.orgEntitled !== false; const disEntitledReasons = data?.disEntitledReasons; @@ -1346,6 +1349,20 @@ function createSignInContent(config) { return signInWrapper; } +const ensureSignedInContextAndOrganizations = async () => { + if (!window.adobeIMS?.isSignedInUser?.()) return; + if (organizationsData && organizationsData.length > 0 && selectedOrganization) return; + try { + const orgs = await fetchOrganizations(); + if (orgs && orgs.length > 0) { + organizationsData = orgs; + await switchOrganization(null); + } + } catch (e) { + // Keep existing fallback org selection when org fetch fails. + } +}; + // ============================================================================ // RETURN PAGE (Previously Created Projects) // ============================================================================ @@ -2106,24 +2123,33 @@ export default async function decorate(block) { }).catch(showFormFromLoading); }; + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + const fetchTemplateEntitlementWithRetry = async (attempts = 4, delayMs = 300) => { + for (let i = 0; i < attempts; i += 1) { + const entitlement = await fetchTemplateEntitlement(); + if (entitlement) return entitlement; + if (i < attempts - 1) { + await sleep(delayMs); + } + } + return null; + }; + // Re-render page after organization change: show loading, then entitlement check (if Request Access) and show correct view. renderPageAfterOrgChange = (sourceContainer) => { setLoadingText(loadingContainer, 'Loading...'); navigateTo(sourceContainer, loadingContainer); - if (requestAccessContainer) { - fetchTemplateEntitlement().then(async (entitlement) => { - if (entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { - const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); - const profile = await window.adobeIMS?.getProfile().catch(() => null); - updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); - navigateTo(loadingContainer, requestAccessContainer); - } else { - runFormOrReturnFlow(); - } - }).catch(() => runFormOrReturnFlow()); - } else { - runFormOrReturnFlow(); - } + fetchTemplateEntitlementWithRetry().then(async (entitlement) => { + if (requestAccessContainer && entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { + const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); + const profile = await window.adobeIMS?.getProfile().catch(() => null); + updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); + navigateTo(loadingContainer, requestAccessContainer); + } else { + runFormOrReturnFlow(); + } + }).catch(() => runFormOrReturnFlow()); }; // Create success card container @@ -2337,17 +2363,16 @@ export default async function decorate(block) { navigateTo(loadingContainer, requestAccessContainer); return; } - // If Request Access config exists, check entitlement first (same as React: !template.userEntitled || !template.orgEntitled -> RequestAccess) - if (requestAccessContainer) { - const entitlement = await fetchTemplateEntitlement(); - if (entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { - lastRequestAccessEntitlement = entitlement; - const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); - const profile = await window.adobeIMS?.getProfile().catch(() => null); - updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); - navigateTo(loadingContainer, requestAccessContainer); - return; - } + // Fetch template data (populates templateData.apis for credential creation) + // and check entitlement (same as React: !template.userEntitled || !template.orgEntitled -> RequestAccess) + const entitlement = await fetchTemplateEntitlement(); + if (requestAccessContainer && entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { + lastRequestAccessEntitlement = entitlement; + const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); + const profile = await window.adobeIMS?.getProfile().catch(() => null); + updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); + navigateTo(loadingContainer, requestAccessContainer); + return; } runFormOrReturnFlow(); }); @@ -2361,8 +2386,40 @@ export default async function decorate(block) { } }; + let signInStateWatcherInterval = null; + let signInStateWatcherTimeout = null; + let isResolvingSignedInState = false; + const stopSignInStateWatcher = () => { + if (signInStateWatcherInterval) { + clearInterval(signInStateWatcherInterval); + signInStateWatcherInterval = null; + } + if (signInStateWatcherTimeout) { + clearTimeout(signInStateWatcherTimeout); + signInStateWatcherTimeout = null; + } + }; + const startSignInStateWatcher = () => { + stopSignInStateWatcher(); + const trySyncSignedInState = () => { + if (signInContainer?.classList.contains('hidden')) { + stopSignInStateWatcher(); + return; + } + if (window.adobeIMS?.isSignedInUser?.()) { + void checkAlreadySignedIn(); + stopSignInStateWatcher(); + } + }; + trySyncSignedInState(); + signInStateWatcherInterval = setInterval(trySyncSignedInState, 250); + signInStateWatcherTimeout = setTimeout(() => { + stopSignInStateWatcher(); + }, 10000); + }; + // Check if user is already signed in when page loads - const checkAlreadySignedIn = () => { + const checkAlreadySignedIn = async () => { // Don't check if we're handling an OAuth callback const hash = window.location.hash; if (hash && hash.includes('access_token=')) { @@ -2371,41 +2428,50 @@ export default async function decorate(block) { // Check if user is already signed in if (window.adobeIMS && window.adobeIMS.isSignedInUser()) { + if (!signInContainer || signInContainer.classList.contains('hidden')) { + return; + } + if (isResolvingSignedInState) { + return; + } + isResolvingSignedInState = true; + // User is already signed in - hide sign-in page and show appropriate content if (signInContainer && !signInContainer.classList.contains('hidden')) { + try { // Navigate from sign-in to loading setLoadingText(loadingContainer, 'Loading...'); navigateTo(signInContainer, loadingContainer, true); const forceParams = getForceRequestAccessParams(); if (forceParams && requestAccessContainer) { - window.adobeIMS?.getProfile().then((profile) => { - updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, forceParams.edgeCase, { selectedOrganization, userEmail: profile?.email }); - navigateTo(loadingContainer, requestAccessContainer); - }).catch(() => { - updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, forceParams.edgeCase, { selectedOrganization }); - navigateTo(loadingContainer, requestAccessContainer); - }); + const profile = await window.adobeIMS?.getProfile().catch(() => null); + updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, forceParams.edgeCase, { selectedOrganization, userEmail: profile?.email }); + navigateTo(loadingContainer, requestAccessContainer); return; } - // Same as React: if !template.userEntitled || !template.orgEntitled -> show RequestAccess (RestrictedAccess or EdgeCase) - if (requestAccessContainer) { - fetchTemplateEntitlement().then(async (entitlement) => { - if (entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { - lastRequestAccessEntitlement = entitlement; - const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); - const profile = await window.adobeIMS?.getProfile().catch(() => null); - updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); - navigateTo(loadingContainer, requestAccessContainer); - return; - } - runFormOrReturnFlow(); - }).catch(() => runFormOrReturnFlow()); - } else { - runFormOrReturnFlow(); + + await ensureSignedInContextAndOrganizations(); + + // Fetch template data (populates templateData.apis for credential creation) + // and check entitlement (same as React: !template.userEntitled || !template.orgEntitled -> RequestAccess) + const entitlement = await fetchTemplateEntitlementWithRetry(); + if (requestAccessContainer && entitlement && (entitlement.userEntitled === false || entitlement.orgEntitled === false)) { + lastRequestAccessEntitlement = entitlement; + const leftCardKey = getRequestAccessLeftCardKey(entitlement, selectedOrganization, credentialData.RequestAccess); + const profile = await window.adobeIMS?.getProfile().catch(() => null); + updateRequestAccessLeftColumn(requestAccessContainer, credentialData.RequestAccess, leftCardKey, { selectedOrganization, userEmail: profile?.email }); + navigateTo(loadingContainer, requestAccessContainer); + return; } + runFormOrReturnFlow(); + } catch (error) { + runFormOrReturnFlow(); + } finally { + isResolvingSignedInState = false; } } + } }; // Check for IMS callback on page load @@ -2414,12 +2480,20 @@ export default async function decorate(block) { // Check when IMS loads if user is already signed in window.addEventListener('adobeIMS:loaded', () => { handleIMSCallback(); - checkAlreadySignedIn(); + void checkAlreadySignedIn(); + startSignInStateWatcher(); + }); + + window.addEventListener('imsReady', () => { + handleIMSCallback(); + void checkAlreadySignedIn(); + startSignInStateWatcher(); }); - // Also check immediately in case IMS is already loaded if (window.adobeIMS && window.adobeIMS.isSignedInUser()) { - checkAlreadySignedIn(); + void checkAlreadySignedIn(); + } else { + startSignInStateWatcher(); } // Copy button functionality for API key diff --git a/hlx_statics/blocks/header/header.css b/hlx_statics/blocks/header/header.css index acc9654a..1b1c2f12 100644 --- a/hlx_statics/blocks/header/header.css +++ b/hlx_statics/blocks/header/header.css @@ -325,6 +325,7 @@ header.global-nav-header > ul#navigation-links > li > button.is-open .spectrum-P margin-top: -6px; margin-left: -1px; max-height: 75vh; + overflow-x: hidden; overflow-y: auto; } @@ -346,17 +347,48 @@ header.global-nav-header > ul#navigation-links > li { } header.global-nav-header ul.nav-sub-menu { - max-width: 230px; + width: max-content; } -header.global-nav-header li .nav-dropdown-item{ +header.global-nav-header .nav-sub-menu .spectrum-Menu-item { + position: relative; + padding-left: 24px !important; +} + +header.global-nav-header .nav-sub-menu .spectrum-Menu-item.is-selected a { + margin-left: 0px; +} + +header.global-nav-header .nav-sub-menu .spectrum-Menu-item a:hover { + background-color: rgba(0, 0, 0, .001); +} + +header.global-nav-header .nav-sub-menu .spectrum-Menu-item.is-selected .spectrum-Menu-checkmark { + position: absolute; + left: 0px; + top: 50%; + transform: translateY(-50%); + display: block; + width: 18px !important; + height: 18px !important; + color: var(--spectrum-global-color-blue-500, #1473e6); + fill: currentColor; +} + +header.global-nav-header li .nav-dropdown-item { display: flex; flex-direction: column; } +header.global-nav-header li .nav-dropdown-item .nav-dropdown-name { + white-space: nowrap; +} + header.global-nav-header li .nav-dropdown-item .nav-dropdown-description { font-size: 12px; + max-width: 200px; text-wrap: initial; + } header.global-nav-header a.nav-dropdown-links { diff --git a/hlx_statics/blocks/iframe/iframe.css b/hlx_statics/blocks/iframe/iframe.css index d509fa7e..6b405f6a 100644 --- a/hlx_statics/blocks/iframe/iframe.css +++ b/hlx_statics/blocks/iframe/iframe.css @@ -47,6 +47,22 @@ main div.iframe-wrapper div.iframe iframe { border: 0; } +main div.iframe-wrapper iframe.iframe-container.youtube-iframe { + width: 560px !important; + height: 315px !important; + max-width: 100%; +} +main div.iframe-wrapper:has(iframe.youtube-iframe) { + background: transparent; +} +@media screen and (max-width: 600px) { + main div.iframe-wrapper iframe.iframe-container.youtube-iframe { + width: 100% !important; + height: auto !important; + aspect-ratio: 16 / 9; + } +} + @media screen and (max-width: 1280px) { main div.iframe-wrapper { padding: 0; diff --git a/hlx_statics/blocks/iframe/iframe.js b/hlx_statics/blocks/iframe/iframe.js index 9e65175b..bfdeec4d 100644 --- a/hlx_statics/blocks/iframe/iframe.js +++ b/hlx_statics/blocks/iframe/iframe.js @@ -167,6 +167,28 @@ export default async function decorate(block) { id: "penpalIframe", }); + + const width = block.getAttribute("data-width"); + const height = block.getAttribute("data-height"); + + const isYouTube = iframeSrcUrl.hostname.includes('youtube.com') || + iframeSrcUrl.hostname.includes('youtu.be'); + if (isYouTube) { + if (!width) iframe.style.width = '560px'; + if (!height) iframe.style.height = '315px'; + iframe.style.border = '0'; + iframeContainer.style.display = 'flex'; + iframeContainer.style.justifyContent = 'center'; + iframe.classList.add('youtube-iframe'); + } + + if (width) { + iframe.style.setProperty('width', width, 'important'); + } + if (height) { + iframe.style.setProperty('height', height, 'important'); + } + penpalScript.onload = () => { iframeContainer.append(iframe); penpalOnLoad(); diff --git a/hlx_statics/blocks/onthispage/onthispage.css b/hlx_statics/blocks/onthispage/onthispage.css index f18bb540..51615e1c 100644 --- a/hlx_statics/blocks/onthispage/onthispage.css +++ b/hlx_statics/blocks/onthispage/onthispage.css @@ -2,6 +2,8 @@ main .onthispage-wrapper a { text-decoration: none; font-size: 16px; color: rgb(0, 84, 182); + overflow-wrap: break-word; + word-break: break-word; } main .onthispage-wrapper a:hover { diff --git a/hlx_statics/icons/B_app_PremierePro.svg b/hlx_statics/icons/premierepro.svg similarity index 100% rename from hlx_statics/icons/B_app_PremierePro.svg rename to hlx_statics/icons/premierepro.svg diff --git a/hlx_statics/scripts/lib-adobeio.js b/hlx_statics/scripts/lib-adobeio.js index 58a73b29..51fbc8a9 100644 --- a/hlx_statics/scripts/lib-adobeio.js +++ b/hlx_statics/scripts/lib-adobeio.js @@ -555,6 +555,14 @@ function activateTab(tabItem, isMainPage) { if (tabItem.closest('.nav-dropdown-popover')){ // if the item is within a dropdown, it needs to find the parent item to be underlined. underlineItem = tabItem.closest('.nav-dropdown-popover'); + const menuItem = tabItem.closest('.spectrum-Menu-item'); + if (menuItem) { + menuItem.classList.add('is-selected'); + const checkmark = ``; + menuItem.insertAdjacentHTML('afterbegin', checkmark); + } } underlineItem.parentElement.classList.add("activeTab"); } diff --git a/hlx_statics/scripts/lib-helix.js b/hlx_statics/scripts/lib-helix.js index 06aef770..a6437eec 100644 --- a/hlx_statics/scripts/lib-helix.js +++ b/hlx_statics/scripts/lib-helix.js @@ -256,6 +256,26 @@ export async function hasContributorsJson() { .catch(() => false); } +export async function getCodePlaygroundJsonPath() { + const metadata = await fetchSiteMetadata(); + const metadataPath = metadata?.get('code-playground'); + if (metadata && !metadataPath) return null; + + const pathPrefix = getMetadata('pathprefix').replace(/^\/|\/$/g, ''); + return metadataPath + ? `${window.location.origin}/${pathPrefix}/${metadataPath}` + : `${window.location.origin}/${pathPrefix}/code-playground.json`; + +} + +export async function hasCodePlaygroundJson() { + const path = await getCodePlaygroundJsonPath(); + if (!path) return false; + return fetch(path) + .then(r => r.ok) + .catch(() => false); +} + /** * Retrieves the nav with the specified name from the config. * @param {string} name The nav name diff --git a/hlx_statics/scripts/scripts.js b/hlx_statics/scripts/scripts.js index abf4dacf..61c32ae4 100644 --- a/hlx_statics/scripts/scripts.js +++ b/hlx_statics/scripts/scripts.js @@ -19,7 +19,9 @@ import { toCamelCase, toClassName, githubActionsBlock, - hasContributorsJson + hasContributorsJson, + hasCodePlaygroundJson, + getCodePlaygroundJsonPath } from './lib-helix.js'; import { @@ -834,7 +836,31 @@ function loadTitle() { } } -function loadPrism(document) { +async function loadPrism(document) { + const codePlaygroundData = {}; + + if (await hasCodePlaygroundJson()) { + const jsonPath = await getCodePlaygroundJsonPath(); + if (jsonPath) { + const response = await fetch(jsonPath); + if (!response.ok) { + console.warn('Network response was not ok:', jsonPath); + return null; + } else { + const json = await response.json(); + json.data.forEach((item) => { + codePlaygroundData[item.key] = item.value; + }); + } + } + else { + return null; + } + } + + const defaultPlaygroundStagingURL = codePlaygroundData['code-playground-staging-url']; + const defaultPlaygroundProductionURL = codePlaygroundData['code-playground-production-url']; + const codeBlocks = document.querySelectorAll('code[class*="language-"], [class*="language-"] code'); if (!codeBlocks.length) return; @@ -864,9 +890,13 @@ function loadPrism(document) { const playgroundMode = pre?.getAttribute('data-playground-mode'); const playgroundExecutionMode = pre?.getAttribute('data-playground-execution-mode'); + const stagingFromPre = pre?.getAttribute('data-playground-url-stage'); + const productionFromPre = pre?.getAttribute('data-playground-url'); + const codePlaygroundStagingURL = stagingFromPre || defaultPlaygroundStagingURL; + const codePlaygroundProductionURL = productionFromPre || defaultPlaygroundProductionURL; const playgroundURL = isStageEnvironment(window.location.host, true) || isLocalHostEnvironment(window.location.host) - ? (pre?.getAttribute('data-playground-url-stage') || pre?.getAttribute('data-playground-url')) - : pre?.getAttribute('data-playground-url'); + ? (codePlaygroundStagingURL || codePlaygroundProductionURL) + : (codePlaygroundProductionURL); if (!sessionId || !playgroundMode || !playgroundExecutionMode || !playgroundURL) return null; const btn = createTag('button', { class: 'try-code-button', diff --git a/hlx_statics/styles/styles.css b/hlx_statics/styles/styles.css index af879317..679c577f 100644 --- a/hlx_statics/styles/styles.css +++ b/hlx_statics/styles/styles.css @@ -290,9 +290,10 @@ main.dev-docs .aside-wrapper .aside { top: 128px; bottom: 0; left: 0; - margin: 32px 32px 0 32px; + margin: 32px 16px 0 16px; max-height: calc(100vh - 128px); - overflow: auto; + overflow-x: hidden; + overflow-y: auto; box-sizing: border-box; padding-bottom: 16px; } diff --git a/package.json b/package.json index 766edead..9487b29e 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ "@octokit/rest": "^22.0.1", "jszip": "^3.10.1", "minimist": "^1.2.6" + }, + "engines": { + "node": ">=24" } }