Skip to content

Commit 9d889d8

Browse files
committed
FEAT: replaced theme screenshots with shortcode
Replaced the wporg-plugins-2024 theme JS gallery with a server-rendered [wporg-plugins-screenshots] shortcode in the Plugin Directory plugin, and added the Gallery Lightbox Enhancements polyfill that ports two pending Gutenberg pull requests (lightbox captions #77477 and the masonry block style #77615). The screenshot UX is now server-side, cacheable by Varnish, works with JS disabled, and renders through core/gallery + core/image blocks instead of bespoke theme JS. The shortcode renders every figure upfront with eager loading, intrinsic width and height attributes, and fetchpriority="high" on the cap-window thumbnails so there is zero layout shift while screenshots decode. Galleries up to nine figures show the full grid; larger ones render a "Show all N screenshots" reveal button that animates max-height on the wrap to full size with a setTimeout fallback so the cap always releases. Layout is deterministic: N=1 single tile, N=2-4 row-aligned grid, N>=5 brick masonry by default with a row-aligned grid upgrade when every screenshot shares an aspect ratio. Aspect ratios and pixel dimensions come from a single Memcached-cached probe over the screenshot URLs (md5-keyed so a revision bump auto-invalidates). Lightbox captions and a generic Masonry block style ship through the polyfill plugin so they self-disable once core lands the upstream PRs. Removed the legacy theme implementation: client/screenshots/, the dedicated theme.js entry, the build:old:js npm script, and the matching SCSS partials. The theme no longer carries any screenshot-specific JavaScript or stylesheets - the shortcode owns the entire flow. Affected: wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/, wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/, wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php, wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/
1 parent 83b5a54 commit 9d889d8

25 files changed

Lines changed: 2094 additions & 940 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
GNU GENERAL PUBLIC LICENSE
2+
Version 2, June 1991
3+
4+
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6+
Everyone is permitted to copy and distribute verbatim copies
7+
of this license document, but changing it is not allowed.
8+
9+
This plugin is licensed under the GNU General Public License v2 (or later).
10+
The full license text is available at:
11+
https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Caption styling for the core image lightbox overlay.
3+
*
4+
* Polyfill for https://github.com/WordPress/gutenberg/pull/77477.
5+
*
6+
* Caption is mounted inside the visible `.lightbox-image-container`
7+
* (the JS picks the one whose <img> is on screen) so it pins to the
8+
* bottom edge of the picture frame, not the bottom of the viewport.
9+
* The visual treatment matches the PR #77477 reference exactly:
10+
* white text with a soft drop shadow on a barely-there bottom-up
11+
* scrim, anchored flush to the image.
12+
*/
13+
14+
/**
15+
* The core zoom-mode `.scrim` is white at 0.9 opacity by default, but
16+
* core CSS doesn't pin it (no `top`/`left` declared). With the two
17+
* `.lightbox-image-container` siblings each `position: relative; top:
18+
* 450px`, the absolute scrim falls through to its normal-flow position
19+
* and lands somewhere around `y: 1444px` — visually below the viewport
20+
* fold. Pin it to the overlay's top-left so the dimmer actually covers
21+
* the page as intended.
22+
*/
23+
.wp-lightbox-overlay .scrim {
24+
top: 0 !important;
25+
left: 0 !important;
26+
}
27+
28+
.wp-lightbox-overlay .lightbox-image-container {
29+
position: relative;
30+
}
31+
32+
.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption {
33+
position: absolute;
34+
bottom: 0;
35+
left: 0;
36+
right: 0;
37+
z-index: 3000001;
38+
margin: 0;
39+
padding: 3.5em 1.25em 1em;
40+
color: #fff;
41+
text-align: center;
42+
font-size: 0.95rem;
43+
font-weight: 500;
44+
line-height: 1.45;
45+
letter-spacing: 0.005em;
46+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
47+
48+
/* Almost invisible scrim — present only to guarantee contrast on
49+
bright photos. The text-shadow does most of the legibility work. */
50+
background: linear-gradient(to top, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 100%);
51+
pointer-events: none;
52+
opacity: 0;
53+
transition: opacity 160ms ease-in;
54+
}
55+
56+
.wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption {
57+
opacity: 1;
58+
transition: opacity 220ms ease-out 200ms;
59+
}
60+
61+
/* Hide the caption when there is no text to show. */
62+
.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption:not(.has-caption) {
63+
visibility: hidden;
64+
}
65+
66+
@media (prefers-reduced-motion: reduce) {
67+
68+
.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption,
69+
.wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption {
70+
transition: none;
71+
}
72+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Augments the core lightbox overlay with a figcaption mounted inside
3+
* the *visible* enlarged-image container, so the caption sits flush
4+
* against the bottom edge of the picture frame — exactly the visual
5+
* contract of https://github.com/WordPress/gutenberg/pull/77477.
6+
*
7+
* Why "visible": core renders two `.lightbox-image-container` siblings.
8+
* One holds the small thumbnail used for the zoom animation hand-off,
9+
* the other holds the full-resolution image. Which one is on screen
10+
* depends on the lifecycle stage (opening, navigating, closing). We
11+
* pick the container whose <img> is actually inside the viewport at
12+
* sync time, mount the caption there, and let CSS pin it to its
13+
* parent's bottom — which is the bottom of the displayed picture.
14+
*
15+
* `data-wp-text` / `data-wp-bind` attributes added after the
16+
* Interactivity runtime parsed the page do not bind, so the caption is
17+
* driven imperatively via a MutationObserver on every overlay <img>.
18+
*/
19+
20+
( function () {
21+
/**
22+
* Whether the element overlaps the current viewport, even partially.
23+
*
24+
* Partial intersection counts; the off-screen sibling sits fully
25+
* past the viewport and gets ruled out cleanly.
26+
*
27+
* @param {HTMLElement} el Element under test.
28+
* @return {boolean}
29+
*/
30+
function isInViewport( el ) {
31+
var rect = el.getBoundingClientRect();
32+
var vh = window.innerHeight || document.documentElement.clientHeight;
33+
var vw = window.innerWidth || document.documentElement.clientWidth;
34+
return (
35+
rect.bottom > 0 &&
36+
rect.top < vh &&
37+
rect.right > 0 &&
38+
rect.left < vw
39+
);
40+
}
41+
42+
/**
43+
* Returns the lightbox image container whose <img> is currently on
44+
* screen, falling back to the first container when nothing matches.
45+
*
46+
* @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
47+
* @return {HTMLElement|null}
48+
*/
49+
function getVisibleContainer( overlay ) {
50+
var containers = overlay.querySelectorAll( '.lightbox-image-container' );
51+
for ( var i = 0; i < containers.length; i++ ) {
52+
var img = containers[ i ].querySelector( 'img' );
53+
if ( img && isInViewport( img ) ) {
54+
return containers[ i ];
55+
}
56+
}
57+
// Fallback: first container — wrong-spot caption beats no caption at all.
58+
return containers[ 0 ] || null;
59+
}
60+
61+
/**
62+
* Moves the caption node into the target container if it isn't
63+
* already attached there.
64+
*
65+
* @param {HTMLElement|null} target Container that should host the caption.
66+
* @param {HTMLElement} caption The figcaption element to move.
67+
*/
68+
function moveCaptionInto( target, caption ) {
69+
if ( target && caption.parentNode !== target ) {
70+
target.appendChild( caption );
71+
}
72+
}
73+
74+
/**
75+
* Returns the overlay's caption node, creating it on first call.
76+
*
77+
* The caption lives at the overlay level so it survives container
78+
* shuffles between open / close. It is moved into whichever
79+
* container is visible at sync time.
80+
*
81+
* @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
82+
* @return {HTMLElement}
83+
*/
84+
function ensureCaption( overlay ) {
85+
var caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' );
86+
if ( ! caption ) {
87+
caption = document.createElement( 'figcaption' );
88+
caption.className = 'wp-lightbox-caption';
89+
overlay.appendChild( caption );
90+
}
91+
return caption;
92+
}
93+
94+
/**
95+
* Reads the active image's alt text and writes it into the caption,
96+
* mounted under the visible container.
97+
*
98+
* @param {HTMLElement} overlay The `.wp-lightbox-overlay` element.
99+
*/
100+
function syncCaption( overlay ) {
101+
var caption = ensureCaption( overlay );
102+
var target = getVisibleContainer( overlay );
103+
moveCaptionInto( target, caption );
104+
105+
// Read alt off the visible image (the off-screen sibling holds stale state).
106+
var img = target ? target.querySelector( 'img' ) : null;
107+
var text = img ? img.getAttribute( 'alt' ) || '' : '';
108+
caption.textContent = text;
109+
caption.classList.toggle( 'has-caption', text !== '' );
110+
}
111+
112+
/**
113+
* Boots the caption pipeline once the overlay is in the DOM.
114+
*/
115+
function init() {
116+
var overlay = document.querySelector( '.wp-lightbox-overlay' );
117+
if ( ! overlay ) {
118+
return;
119+
}
120+
121+
syncCaption( overlay );
122+
123+
/*
124+
* Watch every image inside the overlay; core flips alt/src on
125+
* the active one when the user clicks triggers or arrow-navs.
126+
*/
127+
var imgs = overlay.querySelectorAll( '.lightbox-image-container img' );
128+
imgs.forEach( function ( img ) {
129+
var observer = new MutationObserver( function () {
130+
syncCaption( overlay );
131+
} );
132+
observer.observe( img, { attributes: true, attributeFilter: [ 'alt', 'src' ] } );
133+
} );
134+
135+
/*
136+
* Re-sync when the overlay's active class flips (open / close
137+
* transitions can move which container is on screen).
138+
*/
139+
var classObserver = new MutationObserver( function () {
140+
syncCaption( overlay );
141+
} );
142+
classObserver.observe( overlay, { attributes: true, attributeFilter: [ 'class' ] } );
143+
}
144+
145+
if ( document.readyState === 'loading' ) {
146+
document.addEventListener( 'DOMContentLoaded', init );
147+
} else {
148+
init();
149+
}
150+
} )();
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Masonry style variation for the core/gallery block.
3+
*
4+
* Polyfill for https://github.com/WordPress/gutenberg/pull/77615 — adds
5+
* `is-style-masonry` to the Gallery block's style picker. The variation
6+
* uses CSS multi-column flow so figures keep their natural aspect
7+
* ratios and settle into a brick rhythm; the figure markup is the same
8+
* one core ships, so caption, lightbox, linkTo, random order and
9+
* resolution work unchanged.
10+
*
11+
* Visual treatment is intentionally minimal — no border, no radius, no
12+
* shadow, no hover lift. wp.org gallery patterns render thumbnails as
13+
* bare images; this variation does the same so it sits inside the
14+
* existing UI vocabulary.
15+
*/
16+
17+
.wp-block-gallery.has-nested-images.is-style-masonry {
18+
display: block;
19+
column-gap: var(--wp--style--unstable-gallery-gap, 16px);
20+
column-count: 2;
21+
}
22+
23+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-1 {
24+
column-count: 1;
25+
}
26+
27+
@media (min-width: 600px) {
28+
29+
.wp-block-gallery.has-nested-images.is-style-masonry {
30+
column-count: 3;
31+
}
32+
33+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-2 {
34+
column-count: 2;
35+
}
36+
37+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-3 {
38+
column-count: 3;
39+
}
40+
41+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-4 {
42+
column-count: 4;
43+
}
44+
45+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-5 {
46+
column-count: 5;
47+
}
48+
49+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-6 {
50+
column-count: 6;
51+
}
52+
53+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-7 {
54+
column-count: 7;
55+
}
56+
57+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-8 {
58+
column-count: 8;
59+
}
60+
61+
.wp-block-gallery.has-nested-images.is-style-masonry.columns-default {
62+
column-count: 3;
63+
}
64+
}
65+
66+
/**
67+
* Each figure spans its column with natural height. The aspect-ratio
68+
* override neutralises core/gallery's shared aspect-ratio inline style,
69+
* which would otherwise force every tile to the same shape and undo
70+
* the brick layout.
71+
*/
72+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) {
73+
width: 100%;
74+
margin: 0 0 var(--wp--style--unstable-gallery-gap, 16px);
75+
break-inside: avoid;
76+
display: block;
77+
aspect-ratio: auto !important;
78+
}
79+
80+
/**
81+
* The is-cropped rules in core force a fixed height and object-fit:
82+
* cover on images. That collapses masonry into a grid, so neutralise
83+
* them. !important is required because is-cropped can co-exist with
84+
* is-style-masonry on the wrapper.
85+
*/
86+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) a,
87+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) img {
88+
height: auto !important;
89+
object-fit: initial !important;
90+
}
91+
92+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) img {
93+
display: block;
94+
width: 100%;
95+
}
96+
97+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) button.lightbox-trigger {
98+
cursor: zoom-in;
99+
}
100+
101+
/**
102+
* Default Gallery paints a `backdrop-filter: blur(3px)` strip over the
103+
* bottom of every figure with a figcaption to host a legible scrim.
104+
* In the masonry style captions live in the lightbox, so the strip is
105+
* visible over a clean image and reads as accidental softening.
106+
* Disable it.
107+
*/
108+
.wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image::before {
109+
content: none;
110+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
/**
3+
* Plugin Name: Gallery Lightbox Enhancements
4+
* Description: Polyfill for two pending Gutenberg pull requests: lightbox captions in the Image block (#77477) and a masonry style for the Gallery block (#77615). Self-disables once the upstream changes land in core.
5+
* Version: 1.0.0
6+
* Requires at least: 6.5
7+
* Requires PHP: 7.4
8+
* Author: WordPress.org
9+
* Author URI: https://wordpress.org/
10+
* License: GPL v2 or later
11+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
12+
* Text Domain: gallery-lightbox-enhancements
13+
* Domain Path: /languages
14+
*
15+
* @package GalleryLightboxEnhancements
16+
*/
17+
18+
defined( 'ABSPATH' ) || exit;
19+
20+
require_once __DIR__ . '/includes/class-version-guard.php';
21+
22+
if ( ! GalleryLightboxEnhancements\Version_Guard::should_load() ) {
23+
return;
24+
}
25+
26+
require_once __DIR__ . '/includes/class-masonry-style.php';
27+
require_once __DIR__ . '/includes/class-lightbox-captions.php';
28+
29+
GalleryLightboxEnhancements\Masonry_Style::init();
30+
GalleryLightboxEnhancements\Lightbox_Captions::init();

0 commit comments

Comments
 (0)