From 3e7cea44916e04dc1d5bfe289c04e1f81b5cbd3a Mon Sep 17 00:00:00 2001 From: Ty Clifford Date: Sun, 5 Jul 2026 18:43:51 -0400 Subject: [PATCH] =?UTF-8?q?-=20The=20public=20blog=20theme=20now=20has=20a?= =?UTF-8?q?=20post=20lightbox=20slideshow.=20Changed:=20[themes/neon/asset?= =?UTF-8?q?s/app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/as?= =?UTF-8?q?sets/app.js)=20Scans=20each=20single=20post=E2=80=99s=20.markdo?= =?UTF-8?q?wn=20content.=20Collects=20images,=20native=20videos,=20shortco?= =?UTF-8?q?de/video=20iframes,=20and=20direct=20image/video=20links.=20Ope?= =?UTF-8?q?ns=20media=20in=20a=20lightbox=20with=20Previous/Next,=20count,?= =?UTF-8?q?=20caption,=20Escape=20close,=20and=20arrow-key=20navigation.?= =?UTF-8?q?=20Clicking=20a=20displayed=20image=20advances=20to=20the=20nex?= =?UTF-8?q?t=20item.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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. --- CHANGELOG.md | 1 + themes/neon/assets/app.js | 291 +++++++++++++++++++++++++++++++++++ themes/neon/assets/style.css | 198 ++++++++++++++++++++++++ 3 files changed, 490 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d8c60..8feaae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 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=]` and `[video poster= file=]`. - Added `lone-embed.php` as the generated HTML5 video player target for video shortcodes. diff --git a/themes/neon/assets/app.js b/themes/neon/assets/app.js index db8a0ad..c9f6ac0 100644 --- a/themes/neon/assets/app.js +++ b/themes/neon/assets/app.js @@ -32,4 +32,295 @@ 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 = [ + '', + '
', + '
', + '', + '', + '', + '
', + '
', + '', + '
', + '', + '
', + '

', + '
' + ].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(); }()); diff --git a/themes/neon/assets/style.css b/themes/neon/assets/style.css index ab289cc..26eae00 100644 --- a/themes/neon/assets/style.css +++ b/themes/neon/assets/style.css @@ -346,6 +346,23 @@ h2 { 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 { overflow: auto; padding: 1rem; @@ -499,6 +516,151 @@ h2 { 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 { display: grid; align-content: start; @@ -596,4 +758,40 @@ h2 { .article__header h1 { 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; + } }