- The public blog theme now has a post lightbox slideshow.

Changed:
[themes/neon/assets/app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/app.js)
Scans each single post’s .markdown content.
Collects images, native videos, shortcode/video iframes, and direct 
image/video links.
Opens media in a lightbox with Previous/Next, count, caption, Escape 
close, and arrow-key navigation.
Clicking a displayed image advances to the next item.

[themes/neon/assets/style.css](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/style.css)
Added clickable media styling, embed “View” buttons, and responsive 
lightbox layout.
Works in dark and light modes.

[CHANGELOG.md](/Users/tyemeclifford/Documents/GH/blog/CHANGELOG.md)
Added the feature note.
This commit is contained in:
Ty Clifford
2026-07-05 18:43:51 -04:00
parent d0496c93f3
commit 3e7cea4491
3 changed files with 490 additions and 0 deletions
+1
View File
@@ -12,6 +12,7 @@ All notable changes to Ty Clifford's Content Management System are documented he
- Added Mac client media pagination plus native image preview and audio/video playback. - Added Mac client media pagination plus native image preview and audio/video playback.
- Added Mac client editor spell-check and auto-correct options. - Added Mac client editor spell-check and auto-correct options.
- Added a Mac client Update action for saving existing posts/pages without changing their current draft/published status. - Added a Mac client Update action for saving existing posts/pages without changing their current draft/published status.
- Added a public post lightbox slideshow for images, native videos, video embeds, and direct media links within the same post.
- Added a declared shortcode parser with PHP-array/JSON configuration, including `[youtube=<id>]` and `[video poster=<poster> file=<file>]`. - Added a declared shortcode parser with PHP-array/JSON configuration, including `[youtube=<id>]` and `[video poster=<poster> file=<file>]`.
- Added `lone-embed.php` as the generated HTML5 video player target for video shortcodes. - Added `lone-embed.php` as the generated HTML5 video player target for video shortcodes.
+291
View File
@@ -32,4 +32,295 @@
applyAccent(button.dataset.accent || 'green'); applyAccent(button.dataset.accent || 'green');
}); });
}); });
function initLightbox() {
var article = document.querySelector('.article .markdown');
if (!article) {
return;
}
var imageExtensions = /\.(avif|gif|jpe?g|png|webp|bmp|svg)(?:[?#].*)?$/i;
var videoExtensions = /\.(m4v|mov|mp4|ogv|ogg|webm)(?:[?#].*)?$/i;
var entries = [];
var currentIndex = 0;
var lastFocus = null;
var lightbox = null;
var mediaFrame = null;
var titleNode = null;
var countNode = null;
var captionNode = null;
var closeButton = null;
function mediaTitle(element, fallback) {
return element.getAttribute('alt') ||
element.getAttribute('title') ||
element.getAttribute('aria-label') ||
fallback ||
'Media';
}
function isVideoIframe(iframe, src) {
return Boolean(iframe.closest('.shortcode-embed')) ||
/(?:youtube\.com|youtu\.be|vimeo\.com|player\.vimeo|lone-embed\.php)/i.test(src) ||
videoExtensions.test(src);
}
function addEntry(entry) {
if (!entry.src) {
return;
}
entries.push(entry);
}
article.querySelectorAll('img, video, iframe, a[href]').forEach(function (element) {
var tag = element.tagName.toLowerCase();
if (tag === 'img') {
addEntry({
type: 'image',
src: element.currentSrc || element.src || element.getAttribute('src'),
title: mediaTitle(element, 'Image'),
trigger: element
});
return;
}
if (tag === 'video') {
var source = element.currentSrc || element.getAttribute('src');
var sourceElement = source ? null : element.querySelector('source[src]');
if (!source && sourceElement) {
source = sourceElement.getAttribute('src');
}
addEntry({
type: 'video',
src: source,
poster: element.getAttribute('poster') || '',
title: mediaTitle(element, 'Video'),
trigger: element
});
return;
}
if (tag === 'iframe') {
var iframeSrc = element.getAttribute('src') || '';
if (isVideoIframe(element, iframeSrc)) {
addEntry({
type: 'iframe',
src: iframeSrc,
title: mediaTitle(element, 'Video'),
trigger: element
});
}
return;
}
if (element.querySelector('img, video, iframe')) {
return;
}
var href = element.getAttribute('href') || '';
if (imageExtensions.test(href)) {
addEntry({
type: 'image',
src: href,
title: mediaTitle(element, element.textContent.trim() || 'Image'),
trigger: element
});
} else if (videoExtensions.test(href)) {
addEntry({
type: 'video',
src: href,
title: mediaTitle(element, element.textContent.trim() || 'Video'),
trigger: element
});
}
});
if (!entries.length) {
return;
}
function buildLightbox() {
lightbox = document.createElement('div');
lightbox.className = 'tcms-lightbox';
lightbox.hidden = true;
lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true');
lightbox.setAttribute('aria-label', 'Post media slideshow');
lightbox.innerHTML = [
'<button class="tcms-lightbox__backdrop" type="button" data-lightbox-close aria-label="Close media viewer"></button>',
'<section class="tcms-lightbox__dialog" role="document">',
'<header class="tcms-lightbox__bar">',
'<strong class="tcms-lightbox__title"></strong>',
'<span class="tcms-lightbox__count"></span>',
'<button class="tcms-lightbox__close" type="button" data-lightbox-close aria-label="Close">Close</button>',
'</header>',
'<div class="tcms-lightbox__stage">',
'<button class="tcms-lightbox__nav tcms-lightbox__nav--previous" type="button" data-lightbox-previous aria-label="Previous media">Previous</button>',
'<div class="tcms-lightbox__media"></div>',
'<button class="tcms-lightbox__nav tcms-lightbox__nav--next" type="button" data-lightbox-next aria-label="Next media">Next</button>',
'</div>',
'<p class="tcms-lightbox__caption"></p>',
'</section>'
].join('');
document.body.appendChild(lightbox);
mediaFrame = lightbox.querySelector('.tcms-lightbox__media');
titleNode = lightbox.querySelector('.tcms-lightbox__title');
countNode = lightbox.querySelector('.tcms-lightbox__count');
captionNode = lightbox.querySelector('.tcms-lightbox__caption');
closeButton = lightbox.querySelector('.tcms-lightbox__close');
lightbox.querySelectorAll('[data-lightbox-close]').forEach(function (button) {
button.addEventListener('click', closeLightbox);
});
lightbox.querySelector('[data-lightbox-previous]').addEventListener('click', function () {
showLightboxItem(currentIndex - 1);
});
lightbox.querySelector('[data-lightbox-next]').addEventListener('click', function () {
showLightboxItem(currentIndex + 1);
});
}
function showLightboxItem(index) {
if (!lightbox) {
buildLightbox();
}
if (index < 0) {
index = entries.length - 1;
} else if (index >= entries.length) {
index = 0;
}
currentIndex = index;
var entry = entries[currentIndex];
mediaFrame.textContent = '';
titleNode.textContent = entry.title;
countNode.textContent = (currentIndex + 1) + ' of ' + entries.length;
captionNode.textContent = entry.title;
if (entry.type === 'image') {
var image = document.createElement('img');
image.src = entry.src;
image.alt = entry.title;
image.addEventListener('click', function () {
if (entries.length > 1) {
showLightboxItem(currentIndex + 1);
}
});
mediaFrame.appendChild(image);
return;
}
if (entry.type === 'video') {
var video = document.createElement('video');
video.src = entry.src;
video.controls = true;
video.playsInline = true;
video.preload = 'metadata';
if (entry.poster) {
video.poster = entry.poster;
}
mediaFrame.appendChild(video);
return;
}
var iframe = document.createElement('iframe');
iframe.src = entry.src;
iframe.title = entry.title;
iframe.loading = 'lazy';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
iframe.allowFullscreen = true;
mediaFrame.appendChild(iframe);
}
function openLightbox(index) {
if (!lightbox) {
buildLightbox();
}
lastFocus = document.activeElement;
lightbox.hidden = false;
document.body.classList.add('tcms-lightbox-open');
showLightboxItem(index);
closeButton.focus();
}
function closeLightbox() {
if (!lightbox || lightbox.hidden) {
return;
}
lightbox.hidden = true;
mediaFrame.textContent = '';
document.body.classList.remove('tcms-lightbox-open');
if (lastFocus && typeof lastFocus.focus === 'function') {
lastFocus.focus();
}
}
function openFromEvent(event, index) {
event.preventDefault();
event.stopPropagation();
openLightbox(index);
}
entries.forEach(function (entry, index) {
var trigger = entry.trigger;
var tag = trigger.tagName.toLowerCase();
var button;
if (tag === 'img' || tag === 'a') {
trigger.classList.add('tcms-lightboxable');
trigger.setAttribute('data-lightbox-index', String(index));
if (tag === 'img' && !trigger.hasAttribute('tabindex')) {
trigger.setAttribute('tabindex', '0');
}
if (tag === 'img') {
trigger.setAttribute('role', 'button');
trigger.setAttribute('aria-label', 'Open image viewer');
}
trigger.addEventListener('click', function (event) {
openFromEvent(event, index);
});
trigger.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
openFromEvent(event, index);
}
});
return;
}
button = document.createElement('button');
button.className = 'tcms-lightbox-trigger';
button.type = 'button';
button.textContent = 'View';
button.setAttribute('aria-label', 'Open media viewer');
button.addEventListener('click', function (event) {
openFromEvent(event, index);
});
if (tag === 'iframe' && trigger.parentElement && trigger.parentElement.classList.contains('shortcode-embed')) {
trigger.parentElement.classList.add('tcms-lightbox-host');
trigger.parentElement.appendChild(button);
return;
}
trigger.insertAdjacentElement('afterend', button);
});
document.addEventListener('keydown', function (event) {
if (!lightbox || lightbox.hidden) {
return;
}
if (event.key === 'Escape') {
closeLightbox();
} else if (event.key === 'ArrowRight') {
showLightboxItem(currentIndex + 1);
} else if (event.key === 'ArrowLeft') {
showLightboxItem(currentIndex - 1);
}
});
}
initLightbox();
}()); }());
+198
View File
@@ -346,6 +346,23 @@ h2 {
border: 1px solid var(--line); border: 1px solid var(--line);
} }
.markdown video {
max-width: 100%;
border-radius: var(--radius);
border: 1px solid var(--line);
background: #020403;
}
.tcms-lightboxable {
cursor: zoom-in;
}
.tcms-lightboxable:focus-visible,
.tcms-lightbox-trigger:focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 70%, white);
outline-offset: 4px;
}
.markdown pre { .markdown pre {
overflow: auto; overflow: auto;
padding: 1rem; padding: 1rem;
@@ -499,6 +516,151 @@ h2 {
border: 0; border: 0;
} }
.tcms-lightbox-host {
position: relative;
}
.tcms-lightbox-trigger {
min-height: 0;
margin: 0.45rem 0 1rem;
padding: 0.42rem 0.7rem;
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
color: var(--text);
background: color-mix(in srgb, var(--bg) 84%, transparent);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.24);
backdrop-filter: blur(10px);
}
.tcms-lightbox-host .tcms-lightbox-trigger {
position: absolute;
top: 0.7rem;
right: 0.7rem;
z-index: 2;
margin: 0;
}
body.tcms-lightbox-open {
overflow: hidden;
}
.tcms-lightbox[hidden] {
display: none;
}
.tcms-lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: clamp(0.75rem, 3vw, 2rem);
}
.tcms-lightbox__backdrop {
position: absolute;
inset: 0;
width: 100%;
min-height: 0;
padding: 0;
border: 0;
border-radius: 0;
background: rgba(0, 0, 0, 0.82);
cursor: zoom-out;
}
.tcms-lightbox__dialog {
position: relative;
z-index: 1;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0.75rem;
width: min(1120px, 100%);
max-height: min(860px, 96vh);
border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line));
border-radius: var(--radius);
padding: 0.8rem;
background: color-mix(in srgb, var(--bg) 94%, black);
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.56);
}
:root[data-mode="light"] .tcms-lightbox__dialog {
background: color-mix(in srgb, var(--surface) 96%, white);
}
.tcms-lightbox__bar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.7rem;
align-items: center;
color: var(--muted);
}
.tcms-lightbox__title {
overflow: hidden;
color: var(--text);
text-overflow: ellipsis;
white-space: nowrap;
}
.tcms-lightbox__count {
white-space: nowrap;
}
.tcms-lightbox__close,
.tcms-lightbox__nav {
min-height: 38px;
padding: 0.48rem 0.72rem;
}
.tcms-lightbox__stage {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
min-height: 0;
}
.tcms-lightbox__media {
display: grid;
place-items: center;
min-width: 0;
min-height: min(62vh, 620px);
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: #020403;
}
.tcms-lightbox__media img,
.tcms-lightbox__media video {
display: block;
max-width: 100%;
max-height: min(72vh, 680px);
width: auto;
height: auto;
}
.tcms-lightbox__media img {
cursor: pointer;
}
.tcms-lightbox__media video {
width: 100%;
}
.tcms-lightbox__media iframe {
display: block;
width: min(100%, 1040px);
height: min(68vh, 585px);
border: 0;
}
.tcms-lightbox__caption {
margin: 0;
color: var(--muted);
text-align: center;
}
.side-rail { .side-rail {
display: grid; display: grid;
align-content: start; align-content: start;
@@ -596,4 +758,40 @@ h2 {
.article__header h1 { .article__header h1 {
font-size: 2.25rem; font-size: 2.25rem;
} }
.tcms-lightbox__dialog {
padding: 0.6rem;
}
.tcms-lightbox__bar {
grid-template-columns: 1fr auto;
}
.tcms-lightbox__count {
grid-column: 1 / -1;
order: 3;
}
.tcms-lightbox__stage {
grid-template-columns: 1fr 1fr;
}
.tcms-lightbox__media {
grid-column: 1 / -1;
grid-row: 1;
min-height: 50vh;
}
.tcms-lightbox__nav {
grid-row: 2;
width: 100%;
}
.tcms-lightbox__nav--previous {
grid-column: 1;
}
.tcms-lightbox__nav--next {
grid-column: 2;
}
} }