diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css
index 2ddd30920c..2883d85a46 100644
--- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css
+++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css
@@ -1,14 +1,10 @@
/**
* Caption styling for the core image lightbox overlay.
*
- * Polyfill for https://github.com/WordPress/gutenberg/pull/77477.
- *
- * Caption is mounted inside the visible `.lightbox-image-container`
- * (the JS picks the one whose
is on screen) so it pins to the
- * bottom edge of the picture frame, not the bottom of the viewport.
- * The visual treatment matches the PR #77477 reference exactly:
- * white text with a soft drop shadow on a barely-there bottom-up
- * scrim, anchored flush to the image.
+ * The gallery keeps the core lightbox animation and navigation but
+ * renders the caption as a dedicated bar beneath the active image.
+ * This avoids competing with text-heavy screenshots while still keeping
+ * short captions compact and visually centered.
*/
/**
@@ -25,52 +21,72 @@
}
/*
- * Do NOT set `.lightbox-image-container` to `position: relative`. Core
- * stacks its two containers (zoom-thumbnail + full-res) as absolutely
- * positioned, overlapping siblings; forcing them into normal flow stacks
- * the images vertically and renders the picture twice once both hold a
- * real image (e.g. plugin screenshots). The absolute container is already
- * the caption's containing block — no override needed.
+ * The caption stays at the overlay level. Core already exposes the
+ * active image dimensions through CSS custom properties on the overlay,
+ * so the bar can align itself below the current image without changing
+ * the lightbox container flow.
*/
-.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption {
+.wp-lightbox-overlay > figcaption.wp-lightbox-caption {
position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
+ top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.5rem));
+ left: 50%;
+ transform: translateX(-50%);
z-index: 3000001;
+ box-sizing: border-box;
+ width: min(var(--wporg-lightbox-caption-width, var(--wp--lightbox-container-width)), calc(100% - 1rem));
+ max-width: calc(100% - 1rem);
margin: 0;
- padding: 3.5em 1.25em 1em;
- color: #fff;
- text-align: start;
- font-size: 0.95rem;
- font-weight: 500;
- line-height: 1.45;
- letter-spacing: 0.005em;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
-
- /* Almost invisible scrim — present only to guarantee contrast on
- bright photos. The text-shadow does most of the legibility work. */
- background: linear-gradient(to top, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 100%);
+ padding: 0.75rem 1rem;
+ color: #1e1e1e;
+ text-align: center;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.4;
+ letter-spacing: 0;
+ text-shadow: none;
+ background: rgba(255, 255, 255, 0.98);
+ border: 0;
+ border-radius: 12px;
+ overflow-wrap: anywhere;
pointer-events: none;
opacity: 0;
- transition: opacity 160ms ease-in;
+ transition: opacity 180ms ease-out;
}
-.wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption {
+.wp-lightbox-overlay.active > figcaption.wp-lightbox-caption.has-caption.is-ready {
opacity: 1;
- transition: opacity 220ms ease-out 200ms;
}
/* Hide the caption when there is no text to show. */
-.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption:not(.has-caption) {
+.wp-lightbox-overlay > figcaption.wp-lightbox-caption:not(.has-caption) {
visibility: hidden;
}
+@media (min-width: 960px) {
+
+ .wp-lightbox-overlay > figcaption.wp-lightbox-caption {
+ top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.75rem));
+ padding: 0.875rem 1.25rem;
+ font-size: 0.95rem;
+ }
+}
+
+@media (max-width: 480px), (max-height: 700px) {
+
+ .wp-lightbox-overlay > figcaption.wp-lightbox-caption {
+ top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.375rem));
+ padding: 0.625rem 0.875rem;
+ font-size: 0.8125rem;
+ border-radius: 10px;
+ }
+
+}
+
@media (prefers-reduced-motion: reduce) {
- .wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption,
- .wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption {
+ .wp-lightbox-overlay > figcaption.wp-lightbox-caption,
+ .wp-lightbox-overlay.active > figcaption.wp-lightbox-caption.has-caption {
transition: none;
}
}
diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js
index 158d58cfe7..b5cf1df2ad 100644
--- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js
+++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js
@@ -1,16 +1,12 @@
/**
- * Augments the core lightbox overlay with a figcaption mounted inside
- * the *visible* enlarged-image container, so the caption sits flush
- * against the bottom edge of the picture frame — exactly the visual
- * contract of https://github.com/WordPress/gutenberg/pull/77477.
+ * Augments the core lightbox overlay with a figcaption mounted at the
+ * overlay level, beneath the active image.
*
- * Why "visible": core renders two center-overlapping
- * `.lightbox-image-container` siblings. One holds the small thumbnail
- * used for the zoom animation hand-off, the other the full-resolution
- * image, which paints on top. We mount the caption into that top
- * container — the last one on screen — so it pins to the bottom of the
- * displayed picture and is not occluded by the enlarged image, then let
- * CSS anchor it to the absolutely-positioned container frame.
+ * Core still owns the zoom animation, navigation buttons, and the
+ * active image container geometry. This layer only reads the active
+ * image's caption text, renders a dedicated caption bar below the
+ * image, and switches the text alignment when the rendered caption
+ * grows beyond a short-label treatment.
*
* `data-wp-text` / `data-wp-bind` attributes added after the
* Interactivity runtime parsed the page do not bind, so the caption is
@@ -18,6 +14,9 @@
*/
( function () {
+ var CAPTION_REVEAL_DELAY = 430;
+ var SCREENSHOT_ID_OFFSET = 9000000;
+
/**
* Whether the element overlaps the current viewport, even partially.
*
@@ -57,29 +56,723 @@
return containers[ i ];
}
}
- // Fallback: last container — wrong-spot caption beats no caption at all.
+ // Fallback to the last container because a misplaced caption is better than none.
return containers[ containers.length - 1 ] || null;
}
/**
- * Moves the caption node into the target container if it isn't
- * already attached there.
+ * Keeps the caption as a direct child of the overlay so it can sit in
+ * a dedicated bar beneath the active image without disturbing core's
+ * overlapping lightbox-image containers.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element to move.
+ */
+ function moveCaptionIntoOverlay( overlay, caption ) {
+ var insertBefore = overlay.querySelector( '.screen-reader-text, .scrim' );
+ if ( caption.parentNode !== overlay ) {
+ overlay.insertBefore( caption, insertBefore || null );
+ }
+ }
+
+ /**
+ * Extracts the attachment-style id from a lightbox image class.
+ *
+ * Screenshot gallery images use a high synthetic id range so they can be
+ * distinguished from ordinary site images that should keep core behavior.
+ *
+ * @param {HTMLImageElement|null} img Lightbox image element.
+ * @return {number}
+ */
+ function getImageBlockId( img ) {
+ var match;
+
+ if ( ! img || ! img.className ) {
+ return 0;
+ }
+
+ match = img.className.match( /\bwp-image-(\d+)\b/ );
+
+ if ( ! match ) {
+ return 0;
+ }
+
+ return parseInt( match[1], 10 ) || 0;
+ }
+
+ /**
+ * Whether the current lightbox image belongs to the Plugin Directory
+ * screenshots gallery rather than some other site lightbox.
+ *
+ * @param {HTMLImageElement|null} img Lightbox image element.
+ * @return {boolean}
+ */
+ function isManagedImage( img ) {
+ return getImageBlockId( img ) >= SCREENSHOT_ID_OFFSET;
+ }
+
+ /**
+ * Whether the active overlay currently shows a managed screenshot image.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @return {boolean}
+ */
+ function isManagedOverlay( overlay ) {
+ var target = getVisibleContainer( overlay );
+ var img = target ? target.querySelector( 'img' ) : null;
+
+ return isManagedImage( img );
+ }
+
+ /**
+ * Returns a stable key for the active managed screenshot.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @return {string}
+ */
+ function getActiveImageKey( overlay ) {
+ var target = getVisibleContainer( overlay );
+ var img = target ? target.querySelector( 'img' ) : null;
+
+ if ( ! isManagedImage( img ) ) {
+ return '';
+ }
+
+ return [
+ getImageBlockId( img ),
+ img.currentSrc || img.src || img.getAttribute( 'src' ) || '',
+ ].join( '|' );
+ }
+
+ /**
+ * Whether the lightbox overlay is currently active.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @return {boolean}
+ */
+ function isOverlayActive( overlay ) {
+ return overlay.classList.contains( 'active' );
+ }
+
+ /**
+ * Whether the overlay has finished its current transition.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @return {boolean}
+ */
+ function isOverlaySettled( overlay ) {
+ return overlay.dataset.wporgCaptionSettled === '1';
+ }
+
+ /**
+ * Marks the overlay as still transitioning.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function markOverlayUnsettled( overlay ) {
+ overlay.dataset.wporgCaptionSettled = '0';
+ }
+
+ /**
+ * Marks the overlay as stable enough for geometry-sensitive caption work.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function markOverlaySettled( overlay ) {
+ overlay.dataset.wporgCaptionSettled = '1';
+ }
+
+ /**
+ * Returns whether the first caption reveal is still waiting for core zoom.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @return {boolean}
+ */
+ function isCaptionRevealPending( overlay ) {
+ return overlay.dataset.wporgCaptionRevealPending === '1';
+ }
+
+ /**
+ * Marks the caption as waiting for the first reveal.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function markCaptionRevealPending( overlay ) {
+ overlay.dataset.wporgCaptionRevealPending = '1';
+ overlay.dataset.wporgCaptionRevealKey = getActiveImageKey( overlay );
+ }
+
+ /**
+ * Clears the first reveal waiting state.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function clearCaptionRevealPending( overlay ) {
+ delete overlay.dataset.wporgCaptionRevealPending;
+ delete overlay.dataset.wporgCaptionRevealKey;
+ }
+
+ /**
+ * Clears cached core lightbox dimensions so the next settled sync starts
+ * from the current image rather than a stale transition frame.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function clearOverlayMetricCache( overlay ) {
+ delete overlay.dataset.wporgLightboxBaseKey;
+ delete overlay.dataset.wporgLightboxContainerWidth;
+ delete overlay.dataset.wporgLightboxContainerHeight;
+ delete overlay.dataset.wporgLightboxImageWidth;
+ delete overlay.dataset.wporgLightboxImageHeight;
+ delete overlay.dataset.wporgLightboxScale;
+ delete overlay.dataset.wporgLightboxGeometryApplied;
+ }
+
+ /**
+ * Clears any pending deferred caption sync.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function clearDeferredSync( overlay ) {
+ if ( overlay.wporgCaptionSyncTimer ) {
+ window.clearTimeout( overlay.wporgCaptionSyncTimer );
+ overlay.wporgCaptionSyncTimer = null;
+ }
+ }
+
+ /**
+ * Clears any pending caption reveal.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function clearCaptionReveal( overlay ) {
+ if ( overlay.wporgCaptionRevealTimer ) {
+ window.clearTimeout( overlay.wporgCaptionRevealTimer );
+ overlay.wporgCaptionRevealTimer = null;
+ }
+
+ clearCaptionRevealPending( overlay );
+ }
+
+ /**
+ * Reveals the caption after layout-sensitive sizing has settled.
+ *
+ * Delaying the opacity flip avoids showing the caption while the
+ * image and caption widths are still being recomputed.
*
- * @param {HTMLElement|null} target Container that should host the caption.
- * @param {HTMLElement} caption The figcaption element to move.
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element.
*/
- function moveCaptionInto( target, caption ) {
- if ( target && caption.parentNode !== target ) {
- target.appendChild( caption );
+ function scheduleCaptionReveal( overlay, caption ) {
+ clearCaptionReveal( overlay );
+
+ if ( ! isOverlayActive( overlay ) || ! caption.classList.contains( 'has-caption' ) ) {
+ return;
}
+
+ markCaptionRevealPending( overlay );
+
+ overlay.wporgCaptionRevealTimer = window.setTimeout( function () {
+ overlay.wporgCaptionRevealTimer = null;
+ clearCaptionRevealPending( overlay );
+
+ if ( ! isOverlayActive( overlay ) || ! caption.classList.contains( 'has-caption' ) ) {
+ return;
+ }
+
+ caption.classList.add( 'is-ready' );
+ }, 24 );
+ }
+
+ /**
+ * Hides the caption without touching the current lightbox geometry.
+ *
+ * We intentionally leave core sizing variables as-is during the close
+ * animation so the zoom-back transition does not snap.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element.
+ */
+ function hideCaption( overlay, caption ) {
+ clearCaptionReveal( overlay );
+ caption.classList.remove( 'has-caption', 'is-long-caption', 'is-ready' );
+ caption.style.removeProperty( '--wporg-lightbox-caption-width' );
+ caption.style.removeProperty( '--wporg-lightbox-caption-top' );
+ caption.style.removeProperty( '--wporg-lightbox-caption-max-height' );
+ }
+
+ /**
+ * Returns the number of rendered text lines inside the caption.
+ *
+ * The CSS keeps a stable width for the caption bar, so the rendered
+ * line count reflects the real lightbox layout on both desktop and
+ * narrow mobile viewports.
+ *
+ * @param {HTMLElement} caption The figcaption element.
+ * @return {number}
+ */
+ function getRenderedLineCount( caption ) {
+ var styles = window.getComputedStyle( caption );
+ var lineHeight = parseFloat( styles.lineHeight );
+ var paddingTop = parseFloat( styles.paddingTop ) || 0;
+ var paddingBottom = parseFloat( styles.paddingBottom ) || 0;
+
+ if ( ! lineHeight ) {
+ return 0;
+ }
+
+ return Math.max(
+ 0,
+ ( caption.getBoundingClientRect().height - paddingTop - paddingBottom ) / lineHeight
+ );
+ }
+
+ /**
+ * Whether two rectangles overlap.
+ *
+ * @param {DOMRect} a First rectangle.
+ * @param {DOMRect|null} b Second rectangle.
+ * @return {boolean}
+ */
+ function rectsOverlap( a, b ) {
+ return !! b && ! (
+ a.right <= b.left ||
+ a.left >= b.right ||
+ a.bottom <= b.top ||
+ a.top >= b.bottom
+ );
+ }
+
+ /**
+ * Reads a numeric lightbox size variable off the overlay.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {string} variableName CSS custom property name.
+ * @return {number}
+ */
+ function getOverlayMetric( overlay, variableName ) {
+ var inlineValue = overlay.style.getPropertyValue( variableName );
+ var computedValue = inlineValue || window.getComputedStyle( overlay ).getPropertyValue( variableName );
+ var parsedValue = parseFloat( computedValue );
+
+ return Number.isFinite( parsedValue ) ? parsedValue : 0;
+ }
+
+ /**
+ * Stores the current lightbox image dimensions for the active image and viewport.
+ *
+ * Core recalculates these values when the user navigates between images or
+ * resizes the viewport. We cache the current set so the caption logic can
+ * temporarily shrink the visible image and still restore the original values
+ * before the next measurement pass.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ */
+ function cacheOverlayMetrics( overlay, img ) {
+ var key = ( img ? img.getAttribute( 'src' ) || '' : '' ) +
+ '@' + window.innerWidth + 'x' + window.innerHeight;
+
+ if ( overlay.dataset.wporgLightboxBaseKey === key ) {
+ return;
+ }
+
+ overlay.dataset.wporgLightboxBaseKey = key;
+ overlay.dataset.wporgLightboxContainerWidth = String(
+ getOverlayMetric( overlay, '--wp--lightbox-container-width' )
+ );
+ overlay.dataset.wporgLightboxContainerHeight = String(
+ getOverlayMetric( overlay, '--wp--lightbox-container-height' )
+ );
+ overlay.dataset.wporgLightboxImageWidth = String(
+ getOverlayMetric( overlay, '--wp--lightbox-image-width' )
+ );
+ overlay.dataset.wporgLightboxImageHeight = String(
+ getOverlayMetric( overlay, '--wp--lightbox-image-height' )
+ );
+ overlay.dataset.wporgLightboxScale = String(
+ getOverlayMetric( overlay, '--wp--lightbox-scale' )
+ );
+ }
+
+ /**
+ * Restores the lightbox image dimensions captured from core.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function resetOverlayMetrics( overlay ) {
+ var metrics = {
+ '--wp--lightbox-container-width': overlay.dataset.wporgLightboxContainerWidth,
+ '--wp--lightbox-container-height': overlay.dataset.wporgLightboxContainerHeight,
+ '--wp--lightbox-image-width': overlay.dataset.wporgLightboxImageWidth,
+ '--wp--lightbox-image-height': overlay.dataset.wporgLightboxImageHeight,
+ '--wp--lightbox-scale': overlay.dataset.wporgLightboxScale,
+ };
+
+ Object.keys( metrics ).forEach( function ( variableName ) {
+ if ( ! metrics[ variableName ] ) {
+ return;
+ }
+
+ if ( variableName === '--wp--lightbox-scale' ) {
+ overlay.style.setProperty( variableName, metrics[ variableName ] );
+ return;
+ }
+
+ overlay.style.setProperty( variableName, metrics[ variableName ] + 'px' );
+ } );
+ }
+
+ /**
+ * Reads the rendered image dimensions from the active lightbox image.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ * @return {{width:number,height:number}}
+ */
+ function getRenderedImageMetrics( overlay, img ) {
+ var overlayWidth = getOverlayMetric( overlay, '--wp--lightbox-image-width' );
+ var overlayHeight = getOverlayMetric( overlay, '--wp--lightbox-image-height' );
+ var imageRect = img ? img.getBoundingClientRect() : null;
+
+ if ( overlayWidth && overlayHeight ) {
+ return {
+ width: overlayWidth,
+ height: overlayHeight,
+ };
+ }
+
+ return {
+ width: imageRect ? imageRect.width : getOverlayMetric( overlay, '--wp--lightbox-image-width' ),
+ height: imageRect ? imageRect.height : getOverlayMetric( overlay, '--wp--lightbox-image-height' ),
+ };
+ }
+
+ /**
+ * Whether the active screenshot is portrait-shaped or otherwise very narrow.
+ *
+ * Those screenshots already get a wider caption bar and need the fitting
+ * math to use the intended lightbox geometry, not the current animation
+ * frame, otherwise the open transition can visibly jump mid-flight.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ * @return {boolean}
+ */
+ function isPortraitOrNarrowImage( overlay, img ) {
+ var imageMetrics = getRenderedImageMetrics( overlay, img );
+ var aspectRatio = imageMetrics.width ? imageMetrics.height / imageMetrics.width : 0;
+
+ return aspectRatio > 1.2 || ( imageMetrics.width > 0 && imageMetrics.width < 320 );
+ }
+
+ /**
+ * Shrinks the lightbox image proportionally when a below-image caption would
+ * otherwise fall out of the viewport or into the navigation controls.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ */
+ function fitCaptionWithinViewport( overlay, caption, img ) {
+ var bottomPadding = 16;
+ var attempt = 0;
+ var minContainerScale = 0.5;
+ var nextButton = overlay.querySelector( '.wp-lightbox-navigation-button-next' );
+ var prevButton = overlay.querySelector( '.wp-lightbox-navigation-button-prev' );
+ var maxCaptionBottom = window.innerHeight - bottomPadding;
+ var nextRect = nextButton ? nextButton.getBoundingClientRect() : null;
+ var prevRect = prevButton ? prevButton.getBoundingClientRect() : null;
+ var isPortraitOrNarrow = isPortraitOrNarrowImage( overlay, img );
+ var overflow;
+
+ if ( isPortraitOrNarrow ) {
+ if ( window.innerWidth <= 480 ) {
+ minContainerScale = 0.2;
+ } else if ( window.innerWidth <= 960 || window.innerHeight <= 700 ) {
+ minContainerScale = 0.24;
+ } else {
+ minContainerScale = 0.28;
+ }
+ } else if ( window.innerWidth <= 480 ) {
+ minContainerScale = 0.28;
+ } else if ( window.innerWidth <= 960 || window.innerHeight <= 700 ) {
+ minContainerScale = 0.35;
+ }
+
+ overflow = Math.max(
+ 0,
+ caption.getBoundingClientRect().bottom - maxCaptionBottom
+ );
+
+ if ( rectsOverlap( caption.getBoundingClientRect(), nextRect ) ) {
+ overflow = Math.max(
+ overflow,
+ caption.getBoundingClientRect().bottom - ( nextRect.top - 12 )
+ );
+ }
+
+ if ( rectsOverlap( caption.getBoundingClientRect(), prevRect ) ) {
+ overflow = Math.max(
+ overflow,
+ caption.getBoundingClientRect().bottom - ( prevRect.top - 12 )
+ );
+ }
+
+ while ( overflow > 0 && attempt < 8 ) {
+ var currentContainerWidth = getOverlayMetric( overlay, '--wp--lightbox-container-width' );
+ var currentContainerHeight = getOverlayMetric( overlay, '--wp--lightbox-container-height' );
+ var currentImageWidth = getOverlayMetric( overlay, '--wp--lightbox-image-width' );
+ var currentImageHeight = getOverlayMetric( overlay, '--wp--lightbox-image-height' );
+ var currentLightboxScale = getOverlayMetric( overlay, '--wp--lightbox-scale' );
+ var minContainerHeight = parseFloat( overlay.dataset.wporgLightboxContainerHeight ) * minContainerScale;
+ var newContainerHeight = Math.max(
+ minContainerHeight,
+ currentContainerHeight - ( overflow * 2 )
+ );
+ var scale;
+
+ if (
+ ! currentContainerWidth ||
+ ! currentContainerHeight ||
+ ! currentImageWidth ||
+ ! currentImageHeight ||
+ ! currentLightboxScale ||
+ newContainerHeight === currentContainerHeight
+ ) {
+ return;
+ }
+
+ scale = newContainerHeight / currentContainerHeight;
+
+ overlay.style.setProperty(
+ '--wp--lightbox-container-width',
+ ( currentContainerWidth * scale ) + 'px'
+ );
+ overlay.style.setProperty(
+ '--wp--lightbox-container-height',
+ newContainerHeight + 'px'
+ );
+ overlay.style.setProperty(
+ '--wp--lightbox-image-width',
+ ( currentImageWidth * scale ) + 'px'
+ );
+ overlay.style.setProperty(
+ '--wp--lightbox-image-height',
+ ( currentImageHeight * scale ) + 'px'
+ );
+ overlay.style.setProperty(
+ '--wp--lightbox-scale',
+ currentLightboxScale / scale
+ );
+
+ overflow = Math.max(
+ 0,
+ caption.getBoundingClientRect().bottom - maxCaptionBottom
+ );
+
+ if ( rectsOverlap( caption.getBoundingClientRect(), nextRect ) ) {
+ overflow = Math.max(
+ overflow,
+ caption.getBoundingClientRect().bottom - ( nextRect.top - 12 )
+ );
+ }
+
+ if ( rectsOverlap( caption.getBoundingClientRect(), prevRect ) ) {
+ overflow = Math.max(
+ overflow,
+ caption.getBoundingClientRect().bottom - ( prevRect.top - 12 )
+ );
+ }
+
+ attempt++;
+ }
+ }
+
+ /**
+ * Keeps portrait and very narrow screenshots readable by letting a
+ * text-heavy caption grow wider than the rendered image.
+ *
+ * Short labels stay aligned to the image width. Longer copy can use a
+ * wider bar when the screenshot itself is too narrow to carry a
+ * paragraph-shaped caption comfortably.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ * @param {string} text The caption text.
+ * @param {boolean} promoteForLongCaption Whether a measured paragraph
+ * caption should get extra width.
+ */
+ function applyCaptionWidth( overlay, caption, img, text, promoteForLongCaption ) {
+ var imageMetrics = getRenderedImageMetrics( overlay, img );
+ var imageWidth = imageMetrics.width;
+ var viewportWidth = Math.max( 0, window.innerWidth - 16 );
+ var isPortraitOrNarrow = isPortraitOrNarrowImage( overlay, img );
+ var preferredWidth = imageWidth;
+ var relaxedWidth;
+
+ if ( ! imageWidth || ! viewportWidth ) {
+ caption.style.removeProperty( '--wporg-lightbox-caption-width' );
+ return;
+ }
+
+ if ( isPortraitOrNarrow && ( text.length > 60 || promoteForLongCaption ) ) {
+ relaxedWidth = Math.max(
+ imageWidth,
+ Math.min( 420, imageWidth * 2 ),
+ 300
+ );
+
+ if ( promoteForLongCaption ) {
+ relaxedWidth = Math.max(
+ relaxedWidth,
+ Math.min( 460, imageWidth * 2.3 ),
+ 340
+ );
+ }
+
+ preferredWidth = Math.min( viewportWidth, relaxedWidth );
+ }
+
+ caption.style.setProperty( '--wporg-lightbox-caption-width', preferredWidth + 'px' );
+ }
+
+ /**
+ * Syncs caption width and long-text detection against the current
+ * rendered image size.
+ *
+ * The image may shrink after viewport-fitting, so this pass can be
+ * repeated after geometry changes to keep the final caption width in
+ * step with the visible screenshot.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {HTMLElement} caption The figcaption element.
+ * @param {HTMLImageElement|null} img The active lightbox image.
+ * @param {string} text The caption text.
+ */
+ function syncCaptionPresentation( overlay, caption, img, text ) {
+ var isLongCaption;
+
+ caption.classList.remove( 'is-long-caption' );
+ applyCaptionWidth( overlay, caption, img, text, false );
+
+ isLongCaption = isParagraphCaption( text, caption );
+ if ( isLongCaption ) {
+ applyCaptionWidth( overlay, caption, img, text, true );
+ isLongCaption = isParagraphCaption( text, caption );
+ }
+
+ caption.classList.toggle( 'is-long-caption', isLongCaption );
+ }
+
+ /**
+ * Decides whether the caption should be treated as long-form copy.
+ *
+ * Short product-style captions keep the same centered treatment even if a
+ * narrow mobile viewport wraps them. Longer descriptions still keep that
+ * centered treatment, but we track them separately for width heuristics.
+ *
+ * @param {string} text The caption text.
+ * @param {HTMLElement} caption The figcaption element.
+ * @return {boolean}
+ */
+ function isParagraphCaption( text, caption ) {
+ var renderedLineCount = getRenderedLineCount( caption );
+
+ return renderedLineCount > 2.5 || ( text.length > 220 && renderedLineCount > 2 );
+ }
+
+ /**
+ * Reveals the caption after core's opening zoom has finished.
+ *
+ * Geometry is applied before the first visible zoom frame so the image
+ * does not jump at the end of the animation. Text waits until the zoom
+ * completes, keeping the opening motion focused on the screenshot.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function scheduleDeferredSync( overlay ) {
+ clearDeferredSync( overlay );
+
+ if ( ! isOverlayActive( overlay ) ) {
+ return;
+ }
+
+ markCaptionRevealPending( overlay );
+
+ overlay.wporgCaptionSyncTimer = window.setTimeout( function () {
+ overlay.wporgCaptionSyncTimer = null;
+ markOverlaySettled( overlay );
+ clearOverlayMetricCache( overlay );
+ syncCaption( overlay, false );
+ scheduleCaptionReveal( overlay, ensureCaption( overlay ) );
+ }, CAPTION_REVEAL_DELAY );
+ }
+
+ /**
+ * Queues a two-phase sync so captions do not mutate the lightbox geometry
+ * while core is still animating the current image.
+ *
+ * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ */
+ function requestSettledSync( overlay ) {
+ var shouldSyncImmediately = isOverlaySettled( overlay );
+
+ if ( ! isOverlayActive( overlay ) ) {
+ clearDeferredSync( overlay );
+ markOverlayUnsettled( overlay );
+ clearOverlayMetricCache( overlay );
+ hideCaption( overlay, ensureCaption( overlay ) );
+ return;
+ }
+
+ if ( ! isManagedOverlay( overlay ) ) {
+ clearDeferredSync( overlay );
+ markOverlayUnsettled( overlay );
+ clearOverlayMetricCache( overlay );
+
+ if ( overlay.querySelector( 'figcaption.wp-lightbox-caption' ) ) {
+ hideCaption( overlay, ensureCaption( overlay ) );
+ }
+
+ return;
+ }
+
+ if ( shouldSyncImmediately ) {
+ if ( isCaptionRevealPending( overlay ) ) {
+ var pendingKey = overlay.dataset.wporgCaptionRevealKey || '';
+ var currentKey = getActiveImageKey( overlay );
+
+ clearOverlayMetricCache( overlay );
+ syncCaption( overlay, false );
+
+ if ( currentKey !== pendingKey ) {
+ clearCaptionReveal( overlay );
+ scheduleDeferredSync( overlay );
+ }
+
+ return;
+ }
+
+ clearDeferredSync( overlay );
+ clearCaptionReveal( overlay );
+ clearOverlayMetricCache( overlay );
+ syncCaption( overlay, true );
+ return;
+ }
+
+ clearOverlayMetricCache( overlay );
+ syncCaption( overlay, false );
+ scheduleDeferredSync( overlay );
}
/**
* Returns the overlay's caption node, creating it on first call.
*
* The caption lives at the overlay level so it survives container
- * shuffles between open / close. It is moved into whichever
- * container is visible at sync time.
+ * shuffles between open / close and can be positioned outside the
+ * image frame.
*
* @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
* @return {HTMLElement}
@@ -95,21 +788,86 @@
}
/**
- * Reads the active image's alt text and writes it into the caption,
- * mounted under the visible container.
+ * Reads the active image's alt text and writes it into the caption
+ * bar below the visible image container.
*
* @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
+ * @param {boolean} keepReady Whether an already-visible caption should stay visible.
*/
- function syncCaption( overlay ) {
- var caption = ensureCaption( overlay );
+ function syncCaption( overlay, keepReady ) {
var target = getVisibleContainer( overlay );
- moveCaptionInto( target, caption );
-
// Read alt off the visible image (the off-screen sibling holds stale state).
var img = target ? target.querySelector( 'img' ) : null;
var text = img ? img.getAttribute( 'alt' ) || '' : '';
+ var caption;
+
+ if ( ! isOverlayActive( overlay ) ) {
+ clearDeferredSync( overlay );
+ markOverlayUnsettled( overlay );
+ clearOverlayMetricCache( overlay );
+ caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' );
+
+ if ( caption ) {
+ hideCaption( overlay, caption );
+ }
+
+ return;
+ }
+
+ if ( ! isManagedImage( img ) ) {
+ clearDeferredSync( overlay );
+ markOverlayUnsettled( overlay );
+ clearOverlayMetricCache( overlay );
+ caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' );
+
+ if ( caption ) {
+ hideCaption( overlay, caption );
+ }
+
+ return;
+ }
+
+ caption = ensureCaption( overlay );
+
+ if ( ! isOverlayActive( overlay ) ) {
+ clearDeferredSync( overlay );
+ markOverlayUnsettled( overlay );
+ clearOverlayMetricCache( overlay );
+ hideCaption( overlay, caption );
+ return;
+ }
+
+ moveCaptionIntoOverlay( overlay, caption );
caption.textContent = text;
- caption.classList.toggle( 'has-caption', text !== '' );
+
+ if ( text === '' ) {
+ hideCaption( overlay, caption );
+ return;
+ }
+
+ caption.classList.add( 'has-caption' );
+ if ( ! keepReady ) {
+ caption.classList.remove( 'is-ready' );
+ }
+
+ if ( ! isOverlaySettled( overlay ) ) {
+ return;
+ }
+
+ if ( overlay.dataset.wporgLightboxGeometryApplied !== '1' ) {
+ cacheOverlayMetrics( overlay, img );
+ resetOverlayMetrics( overlay );
+ syncCaptionPresentation( overlay, caption, img, text );
+ fitCaptionWithinViewport( overlay, caption, img );
+ syncCaptionPresentation( overlay, caption, img, text );
+ overlay.dataset.wporgLightboxGeometryApplied = '1';
+ }
+
+ syncCaptionPresentation( overlay, caption, img, text );
+
+ if ( keepReady ) {
+ caption.classList.add( 'is-ready' );
+ }
}
/**
@@ -121,7 +879,8 @@
return;
}
- syncCaption( overlay );
+ markOverlayUnsettled( overlay );
+ requestSettledSync( overlay );
/*
* Watch every image inside the overlay; core flips alt/src on
@@ -130,7 +889,7 @@
var imgs = overlay.querySelectorAll( '.lightbox-image-container img' );
imgs.forEach( function ( img ) {
var observer = new MutationObserver( function () {
- syncCaption( overlay );
+ requestSettledSync( overlay );
} );
observer.observe( img, { attributes: true, attributeFilter: [ 'alt', 'src' ] } );
} );
@@ -140,9 +899,24 @@
* transitions can move which container is on screen).
*/
var classObserver = new MutationObserver( function () {
- syncCaption( overlay );
+ if ( isOverlayActive( overlay ) ) {
+ requestSettledSync( overlay );
+ return;
+ }
+
+ requestSettledSync( overlay );
} );
classObserver.observe( overlay, { attributes: true, attributeFilter: [ 'class' ] } );
+
+ window.addEventListener( 'resize', function () {
+ requestSettledSync( overlay );
+ } );
+
+ if ( window.visualViewport ) {
+ window.visualViewport.addEventListener( 'resize', function () {
+ requestSettledSync( overlay );
+ } );
+ }
}
if ( document.readyState === 'loading' ) {
diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css
index 6c221657c9..f7b8dd1ba3 100644
--- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css
+++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css
@@ -96,6 +96,11 @@
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) button.lightbox-trigger {
cursor: zoom-in;
+ /* Limit hover work to opacity so tall masonry tiles do not visually
+ twitch when the trigger fades in. */
+ -webkit-backdrop-filter: none;
+ backdrop-filter: none;
+ will-change: opacity;
}
/**
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php
index 105c678dfc..5a33a8bb26 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php
@@ -468,8 +468,6 @@ public function register_shortcodes() {
add_shortcode( Shortcodes\Release_Confirmation::SHORTCODE, array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'display' ) );
add_action( 'template_redirect', array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'template_redirect' ) );
-
- add_filter( 'wp_resource_hints', array( __NAMESPACE__ . '\Shortcodes\Screenshots', 'add_resource_hints' ), 10, 2 );
}
/**
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css b/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css
index 418085e036..95bade1d0c 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css
@@ -217,6 +217,12 @@
#screenshots .wp-block-gallery.is-style-screenshots figure.wp-block-image button.lightbox-trigger {
cursor: zoom-in;
+ /* Core's frosted trigger repaints the underlying thumbnail on hover.
+ Keep the control, but make it a plain opacity change for smoother
+ screenshot-grid interaction. */
+ -webkit-backdrop-filter: none;
+ backdrop-filter: none;
+ will-change: opacity;
}
/**
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js b/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js
index 231d6bc6df..ae79dcee3b 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js
@@ -77,9 +77,9 @@
/*
* Some screenshots end up "loaded" by the browser but with zero
- * natural dimensions — typically Photon returning an empty 200,
- * or a partial / pending response that the network stack never
- * settles. The load / error events don't always fire for those,
+ * natural dimensions, usually after a zero-byte or partial response
+ * that the network stack never settles. The load / error events
+ * don't always fire for those,
* so re-sweep after a short delay and hide anything that's still
* degenerate. The `markBroken` guard against `!complete` keeps
* lazy figures past the fold safe from this sweep.
diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php
index 75fbf13d73..fc4038465b 100644
--- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php
+++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php
@@ -180,34 +180,6 @@ public static function strip_layout_classes( $content ) {
return $processor->get_updated_html();
}
- /**
- * Adds preconnect / dns-prefetch hints to the Photon CDN host on
- * single-plugin pages so the browser can warm up the TLS handshake
- * while the page HTML is still streaming. Saves ~50–150 ms on the
- * first thumbnail paint for cold visitors. Hooked from
- * `class-plugin-directory.php` via the `wp_resource_hints` filter.
- *
- * @param array $urls Resource hint URLs already queued for $relation_type.
- * @param string $relation_type One of preconnect / dns-prefetch / prerender / prefetch.
- * @return array
- */
- public static function add_resource_hints( $urls, $relation_type ) {
- if ( ! is_singular( 'plugin' ) ) {
- return $urls;
- }
-
- if ( 'preconnect' === $relation_type ) {
- $urls[] = array(
- 'href' => 'https://i0.wp.com',
- 'crossorigin' => 'anonymous',
- );
- } elseif ( 'dns-prefetch' === $relation_type ) {
- $urls[] = 'https://i0.wp.com';
- }
-
- return $urls;
- }
-
/**
* Enqueues the shortcode's own CSS and toggle script. Assets live
* under the plugin's `css/` and `js/` directories alongside the
@@ -363,14 +335,12 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal
)
);
- $srcset = self::photon_srcset( $src );
- $class = 'wp-block-image size-large';
+ $class = 'wp-block-image size-large';
// Record the full-resolution source and intrinsic dimensions for the
- // lightbox-state repair in fix_lightbox_metadata(). The grid thumbnail
- // loads a Photon-shrunk srcset candidate, so core (which has no real
- // attachment to query) would otherwise enlarge that small image; this
- // hands the lightbox the lossless original at its true size.
+ // lightbox-state repair in fix_lightbox_metadata(). Screenshot assets
+ // have no real attachment for core to query, so this hands the
+ // lightbox the original `ps.w.org` image at its true size.
self::$lightbox_meta[ (int) $id ] = array(
'url' => $src,
'width' => ( is_array( $dimensions ) && ! empty( $dimensions[0] ) ) ? (int) $dimensions[0] : 'none',
@@ -411,11 +381,10 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal
$src
);
$figure .= sprintf(
- '
',
+ '
',
$src,
esc_attr( $alt ),
$id,
- $srcset,
$dim_attrs,
esc_attr( $priority )
);
@@ -449,11 +418,9 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal
*
* The empty `uploadedSrc` leaves the lightbox with no full-resolution
* image to enlarge, and the `'none'` dimensions make core's view
- * script fall back to the *thumbnail's* natural size — which on
- * production is a Photon-shrunk srcset candidate (≤900px, often the
- * 300px tile). The enlarged view therefore renders tiny. On
- * environments without Photon the thumbnail is the full-resolution
- * original, which is why the bug is invisible on local / staging.
+ * script fall back to the thumbnail's natural size. The rendered
+ * thumbnail may be constrained by layout, but the enlarged view should
+ * always use the original screenshot and its recorded dimensions.
*
* Core keys its lightbox metadata by a per-render `uniqid()` (exposed
* on the figure's `data-wp-context`), not by the attachment id, so the
@@ -461,9 +428,8 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal
* rendered markup and re-set the affected fields. `wp_interactivity_state()`
* merges with `array_replace_recursive()` (later call wins), and this
* filter runs at priority 20 — after core's priority-15 pass — so the
- * corrected values override the broken ones. `lightboxSrcset` is
- * cleared so the enlarged image loads the lossless original rather than
- * a capped Photon candidate.
+ * corrected values override the broken ones. `lightboxSrcset` is cleared
+ * so the enlarged image loads the original screenshot URL.
*
* @param string $block_content Rendered Image block markup.
* @param array $parsed_block Parsed block, including `attrs['id']`.
@@ -554,54 +520,6 @@ protected static function wrap_with_show_all_button( $rendered_gallery, $count )
. '';
}
- /**
- * Builds a Photon-powered `srcset` (and matching `sizes`) attribute string
- * for a `ps.w.org` screenshot URL. Returns an empty string when the source
- * URL is not on `ps.w.org`, so the original `src` is used unchanged.
- *
- * Plugin authors upload screenshots at full resolution but we render them
- * inside a 3-column grid, so the browser otherwise downloads the full
- * asset (often 300–800 KB) only to scale it down to a ~250 px tile.
- * Routing the URL through `i0.wp.com` (Photon) returns a re-encoded,
- * width-bound copy at ~10× smaller payload — see
- * https://developer.wordpress.com/docs/photon/ for the resize/optim
- * options. The grid thumbnail therefore loads a small Photon candidate;
- * the lightbox is pointed back at the full-resolution `ps.w.org` original
- * separately by {@see self::fix_lightbox_metadata()} so users still get
- * the lossless image when they enlarge a screenshot.
- *
- * @param string $src Original asset URL.
- * @return string Attribute fragment ready to interpolate into `
`,
- * including the leading space, or empty string.
- */
- protected static function photon_srcset( $src ) {
- if ( ! preg_match( '#^https?://ps\.w\.org/#', $src ) ) {
- return '';
- }
-
- // Photon (i0.wp.com) only runs on production and staging. In local
- // or other environments the proxy may not be reachable, which would
- // leave the gallery silently empty until the cold cache warmed up.
- // Fall back to the unoptimised `ps.w.org` URL there.
- $env = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production';
- if ( 'production' !== $env && 'staging' !== $env ) {
- return '';
- }
-
- $photon_base = preg_replace( '#^https?://#', 'https://i0.wp.com/', $src );
- $widths = array( 300, 600, 900 );
- $srcset = array();
-
- foreach ( $widths as $width ) {
- $srcset[] = add_query_arg( 'w', $width, $photon_base ) . ' ' . $width . 'w';
- }
-
- return sprintf(
- ' srcset="%1$s" sizes="(max-width: 599px) 50vw, 33vw"',
- esc_attr( implode( ', ', $srcset ) )
- );
- }
-
/**
* Whether the gallery should swap from brick masonry to row-aligned grid.
*