From 6daa5b636182d40d3e956c40c926277c4bd19bbb Mon Sep 17 00:00:00 2001 From: James Zhou Date: Fri, 7 Nov 2025 21:30:18 +1100 Subject: [PATCH 1/5] feat: add long-form progress affordances --- scripts/scripts.js | 112 +++++++++++++++++++++++++++++++++++++++++++++ styles/styles.css | 109 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/scripts/scripts.js b/scripts/scripts.js index 7b8f62b97..56c10b1e0 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -31,6 +31,8 @@ const AUDIENCES = { // define your custom audiences here as needed }; +const LONG_FORM_TEMPLATES = ['docs-template', 'guides-template', 'blog-template']; + window.hlx.plugins.add('performance', { condition: () => window.name.includes('performance'), load: 'eager', @@ -183,6 +185,113 @@ export function addMessageBoxOnGuideTemplate(main) { main.append(messageBox); } +const isLongFormPage = () => LONG_FORM_TEMPLATES + .some((tplClass) => document.body.classList.contains(tplClass)); + +const hasScrollableLongFormContent = () => { + const scrollable = document.documentElement.scrollHeight - window.innerHeight; + return scrollable > window.innerHeight * 0.35; +}; + +function initReadingProgress() { + if (!isLongFormPage() || document.querySelector('.reading-progress')) return; + + const progressBar = createTag('div', { + class: 'reading-progress', + 'aria-hidden': 'true', + }); + const fill = createTag('span', { class: 'reading-progress-fill' }); + progressBar.append(fill); + document.body.prepend(progressBar); + + const updateProgress = () => { + const scrollable = document.documentElement.scrollHeight - window.innerHeight; + const canScroll = scrollable > 0; + const isLongEnough = hasScrollableLongFormContent(); + progressBar.classList.toggle('is-hidden', !isLongEnough); + if (!canScroll || !isLongEnough) { + fill.style.transform = 'scaleX(0)'; + return; + } + const progress = Math.min(window.scrollY / scrollable, 1); + fill.style.transform = `scaleX(${progress})`; + }; + + updateProgress(); + + let ticking = false; + const handleScroll = () => { + if (!ticking) { + window.requestAnimationFrame(() => { + updateProgress(); + ticking = false; + }); + ticking = true; + } + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', updateProgress); +} + +function initBackToTopButton() { + if (!isLongFormPage() || document.querySelector('.back-to-top-button')) return; + + const button = createTag('button', { + type: 'button', + class: 'back-to-top-button', + 'aria-label': 'Back to top', + }); + const icon = createTag('span', { + class: 'back-to-top-button-icon', + 'aria-hidden': 'true', + }); + const label = createTag('span', { class: 'back-to-top-button-label' }, 'Back to top'); + button.append(icon, label); + document.body.append(button); + + const prefersReducedMotionQuery = window.matchMedia + ? window.matchMedia('(prefers-reduced-motion: reduce)') + : null; + let prefersReducedMotion = prefersReducedMotionQuery?.matches; + const updateMotionPreference = (event) => { + prefersReducedMotion = event.matches; + }; + if (prefersReducedMotionQuery?.addEventListener) { + prefersReducedMotionQuery.addEventListener('change', updateMotionPreference); + } else if (prefersReducedMotionQuery?.addListener) { + prefersReducedMotionQuery.addListener(updateMotionPreference); + } + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: prefersReducedMotion ? 'auto' : 'smooth', + }); + button.blur(); + }; + + button.addEventListener('click', scrollToTop); + + const toggleVisibility = () => { + const canScroll = hasScrollableLongFormContent(); + if (!canScroll) { + button.classList.remove('is-visible'); + button.setAttribute('aria-hidden', 'true'); + button.setAttribute('tabindex', '-1'); + return; + } + button.removeAttribute('aria-hidden'); + button.removeAttribute('tabindex'); + const shouldShow = window.scrollY > window.innerHeight * 0.8; + button.classList.toggle('is-visible', shouldShow); + }; + + toggleVisibility(); + window.addEventListener('scroll', toggleVisibility, { passive: true }); + window.addEventListener('resize', toggleVisibility); +} + export function addHeadingAnchorLink(elem) { const link = document.createElement('a'); link.setAttribute('href', `#${elem.id || ''}`); @@ -777,6 +886,9 @@ async function loadLazy(doc) { loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); + initReadingProgress(); + initBackToTopButton(); + if (getMetadata('supressframe')) { doc.querySelector('header').remove(); doc.querySelector('footer').remove(); diff --git a/styles/styles.css b/styles/styles.css index c70cdd39a..9620df88e 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -277,6 +277,115 @@ body.appear { display: unset; } +.reading-progress { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: rgb(104 112 236 / 15%); + z-index: 10000; + pointer-events: none; + overflow: hidden; + opacity: 1; + transition: opacity 0.2s var(--cubic-bezier-1); +} + +.reading-progress.is-hidden { + opacity: 0; +} + +.reading-progress-fill { + display: block; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + var(--color-accent-blue, var(--spectrum-blue)) 0%, + var(--dark-spectrum-blue) + ); + transform-origin: left center; + transform: scaleX(0); + transition: transform 0.18s var(--cubic-bezier-1); +} + +.back-to-top-button { + position: fixed; + bottom: var(--spacing-s); + right: var(--spacing-s); + display: inline-flex; + align-items: center; + gap: var(--spacing-xxs); + padding: 10px 16px; + border-radius: 999px; + background: var(--color-white); + border: 1px solid rgb(104 112 236 / 35%); + color: var(--color-font-grey); + font-size: var(--type-body-s-size); + font-weight: 600; + box-shadow: 0 18px 32px rgb(0 0 0 / 15%); + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 0.25s var(--cubic-bezier-2), + transform 0.25s var(--cubic-bezier-2), + box-shadow 0.25s var(--cubic-bezier-2); + z-index: 10001; +} + +.back-to-top-button.is-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.back-to-top-button:focus-visible { + outline: 2px solid var(--color-accent-blue, var(--spectrum-blue)); + outline-offset: 4px; +} + +.back-to-top-button-icon { + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--color-accent-blue, var(--spectrum-blue)); + color: var(--color-white); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgb(255 255 255 / 30%); +} + +.back-to-top-button-icon::before { + content: ''; + width: 6px; + height: 6px; + border-left: 2px solid currentcolor; + border-top: 2px solid currentcolor; + transform: rotate(45deg); + margin-top: 2px; +} + +.back-to-top-button-label { + white-space: nowrap; +} + +@media screen and (width <= 600px) { + .back-to-top-button { + bottom: var(--spacing-xs); + right: var(--spacing-xs); + padding: 8px 12px; + } +} + +@media (prefers-reduced-motion: reduce) { + .reading-progress-fill, + .reading-progress, + .back-to-top-button { + transition: none; + } +} + /* block style for container width, max-width */ .block.contained, .section.content > div, From 36f72a4790f728e4f975b245b723666e24fb25f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Nov 2025 10:38:50 +0000 Subject: [PATCH 2/5] chore(docs): update admin API docs [skip ci] --- docs/admin-preview.html | 4 ++-- docs/admin.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/admin-preview.html b/docs/admin-preview.html index ae4b42d7c..725fb377e 100644 --- a/docs/admin-preview.html +++ b/docs/admin-preview.html @@ -139,11 +139,11 @@ 55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864 -231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688, -104.0616 -231.873,-231.248 z - " fill="currentColor">

AEM Admin API (Feature Preview) (12.100.18)

Download OpenAPI specification:

License: Apache 2.0

AEM Admin API (Feature Preview) (12.100.19)

Download OpenAPI specification:

License: Apache 2.0

AEM Admin API is used to manage the lifecycle of content and code.