Okay Wish I could say I know exactly. Will do better next time promise lol

This commit is contained in:
2026-02-26 17:47:04 -07:00
parent 2fa50b001c
commit 9e51aac570
727 changed files with 217309 additions and 5801 deletions

View File

@@ -0,0 +1,437 @@
/**
* Image Gallery Styles for MkDocs
*
* Ensures photo blocks from landing pages render properly in documentation.
* Mirrors video-player.css structure with green/blue photo theme.
*/
/* Photo block container */
.photo-block {
margin: 2rem auto;
max-width: 100%;
}
.photo-block img {
width: 100%;
height: auto;
max-width: 100%;
border-radius: 8px;
display: block;
background: #f0f0f0;
}
[data-md-color-scheme="slate"] .photo-block img {
background: #1b2838;
}
/* Photo caption */
.photo-caption {
margin-top: 0.5rem;
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.6);
text-align: center;
font-style: italic;
}
[data-md-color-scheme="slate"] .photo-caption {
color: rgba(255, 255, 255, 0.6);
}
/* Photo card block */
.photo-card-block {
margin: 2rem auto;
max-width: 480px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.photo-card-block:hover {
transform: translateY(-2px);
}
.photo-card-block a {
text-decoration: none !important;
color: inherit !important;
}
/* Photo card gallery button */
.photo-card-gallery-btn:active {
transform: scale(0.96);
}
/* Photo album block */
.photo-album-block {
margin: 2rem auto;
max-width: 900px;
}
.photo-album-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: inherit;
}
/* Photo album grid */
.photo-album-grid {
display: grid;
gap: 8px;
}
.photo-album-grid.cols-2 {
grid-template-columns: repeat(2, 1fr);
}
.photo-album-grid.cols-3 {
grid-template-columns: repeat(3, 1fr);
}
.photo-album-grid.cols-4 {
grid-template-columns: repeat(4, 1fr);
}
/* Photo album cell */
.photo-album-cell {
position: relative;
overflow: hidden;
border-radius: 6px;
background: #f0f0f0;
aspect-ratio: 1;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.photo-album-cell:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
[data-md-color-scheme="slate"] .photo-album-cell {
background: #1b2838;
}
.photo-album-cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.photo-album-cell a {
display: block;
width: 100%;
height: 100%;
}
/* Album "View full album" link */
.photo-album-footer {
text-align: center;
margin-top: 1rem;
}
.photo-album-footer a {
color: #43cea2;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
}
.photo-album-footer a:hover {
text-decoration: underline;
}
[data-md-color-scheme="slate"] .photo-album-footer a {
color: #43cea2;
}
/* Loading state */
.photo-loading {
padding: 2.5rem;
text-align: center;
color: rgba(0, 0, 0, 0.45);
font-size: 0.875rem;
}
[data-md-color-scheme="slate"] .photo-loading {
color: rgba(255, 255, 255, 0.45);
}
/* Error state */
.photo-error {
padding: 2.5rem;
text-align: center;
background: #fff3f3;
border: 1px solid #ffccc7;
border-radius: 8px;
color: #cf1322;
}
[data-md-color-scheme="slate"] .photo-error {
background: rgba(207, 19, 34, 0.1);
border-color: rgba(207, 19, 34, 0.3);
}
.photo-error svg {
width: 48px;
height: 48px;
margin-bottom: 0.75rem;
}
.photo-error p {
margin: 0;
font-weight: 600;
}
/* Photo placeholder (shown during GrapesJS export before hydration) */
.photo-placeholder {
aspect-ratio: 3/2;
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
color: #fff;
padding: 1.5rem;
text-align: center;
}
.photo-placeholder svg {
width: 64px;
height: 64px;
margin-bottom: 1rem;
opacity: 0.9;
}
.photo-placeholder p {
margin: 0;
}
/* Responsive */
@media (max-width: 768px) {
.photo-block,
.photo-card-block,
.photo-album-block {
margin: 1.5rem auto;
}
.photo-block img {
border-radius: 4px;
}
.photo-card-block {
max-width: 100%;
}
.photo-album-grid.cols-4 {
grid-template-columns: repeat(3, 1fr);
}
.photo-album-grid.cols-3 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.photo-album-grid.cols-4,
.photo-album-grid.cols-3 {
grid-template-columns: repeat(2, 1fr);
}
}
/* ─── Photo Album Carousel ─────────────────────────────────────────── */
.photo-album-carousel {
margin: 2rem auto;
max-width: 900px;
position: relative;
outline: none;
}
.carousel-viewport {
position: relative;
overflow: hidden;
border-radius: 8px;
background: #f0f0f0;
}
[data-md-color-scheme="slate"] .carousel-viewport {
background: #1b2838;
}
.carousel-track {
display: flex;
transition: transform 0.4s ease;
}
.carousel-slide {
flex: 0 0 100%;
min-width: 0;
}
.carousel-slide a {
display: block;
width: 100%;
text-decoration: none;
}
.carousel-slide img {
width: 100%;
height: auto;
max-height: 560px;
object-fit: contain;
display: block;
background: #000;
}
/* Navigation arrows */
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 20px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s, background 0.2s;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
padding: 0;
}
.carousel-viewport:hover .carousel-btn {
opacity: 1;
}
.carousel-btn:hover {
background: rgba(0, 0, 0, 0.75);
}
.carousel-btn-prev {
left: 12px;
}
.carousel-btn-next {
right: 12px;
}
/* Dot indicators */
.carousel-dots {
display: flex;
justify-content: center;
gap: 8px;
padding: 12px 0 4px;
}
.carousel-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.2);
cursor: pointer;
padding: 0;
transition: background 0.2s, transform 0.2s;
}
.carousel-dot:hover {
background: rgba(0, 0, 0, 0.4);
}
.carousel-dot.active {
background: #43cea2;
transform: scale(1.2);
}
[data-md-color-scheme="slate"] .carousel-dot {
background: rgba(255, 255, 255, 0.2);
}
[data-md-color-scheme="slate"] .carousel-dot:hover {
background: rgba(255, 255, 255, 0.4);
}
[data-md-color-scheme="slate"] .carousel-dot.active {
background: #43cea2;
}
/* Responsive: arrows always visible on mobile (no hover) */
@media (max-width: 768px) {
.carousel-btn {
opacity: 1;
width: 36px;
height: 36px;
font-size: 16px;
}
.carousel-btn-prev {
left: 8px;
}
.carousel-btn-next {
right: 8px;
}
.carousel-slide img {
max-height: 400px;
}
.photo-album-carousel {
margin: 1.5rem auto;
}
}
@media (max-width: 480px) {
.carousel-slide img {
max-height: 300px;
}
.carousel-dots {
gap: 6px;
}
.carousel-dot {
width: 8px;
height: 8px;
}
}
/* Ensure photos respect content width in Material theme */
.md-content .photo-block,
.md-content .photo-card-block,
.md-content .photo-album-block,
.md-content .photo-album-carousel {
max-width: 100%;
}
/* Accessibility: Focus styles */
.photo-album-cell a:focus,
.photo-card-block a:focus {
outline: 2px solid var(--md-primary-fg-color);
outline-offset: 2px;
}
.carousel-btn:focus-visible {
outline: 2px solid var(--md-primary-fg-color);
outline-offset: 2px;
opacity: 1;
}
.carousel-dot:focus-visible {
outline: 2px solid var(--md-primary-fg-color);
outline-offset: 2px;
}
.carousel-slide a:focus-visible {
outline: 2px solid var(--md-primary-fg-color);
outline-offset: -2px;
}

View File

@@ -0,0 +1,611 @@
/**
* Image Gallery Hydration for MkDocs
*
* Automatically converts .photo-block, .photo-card-block, and .photo-album-block
* placeholders into actual images/galleries when landing pages with photo blocks
* are exported to MkDocs.
*
* Uses the same MEDIA_API_URL and PUBLIC_URL globals as video-player.js
* (injected by hooks/env_config_hook.py).
*/
(function() {
'use strict';
// Configuration — same globals as video-player.js
var MEDIA_API_URL = window.MEDIA_API_URL || 'http://localhost:4100';
var PUBLIC_URL = window.PUBLIC_URL || 'http://localhost:3000';
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(unsafe) {
return String(unsafe)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Format view count (e.g. 1200 -> "1.2K views")
*/
function formatViewCount(count) {
if (!count || count <= 0) return '0 views';
if (count === 1) return '1 view';
if (count < 1000) return count + ' views';
if (count < 1000000) return (count / 1000).toFixed(1).replace(/\.0$/, '') + 'K views';
return (count / 1000000).toFixed(1).replace(/\.0$/, '') + 'M views';
}
/**
* Get gallery URL for a photo
*/
function getGalleryUrl(photoId) {
return PUBLIC_URL + '/gallery?expanded=photo-' + photoId;
}
/**
* Show error state on an element
*/
function showError(el, message) {
el.innerHTML =
'<div class="photo-error">' +
'<svg fill="currentColor" viewBox="0 0 20 20">' +
'<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />' +
'</svg>' +
'<p>' + escapeHtml(message) + '</p>' +
'</div>';
}
/**
* Fetch photo metadata from Media API
*/
async function fetchPhotoMetadata(photoId) {
try {
var response = await fetch(MEDIA_API_URL + '/api/public/photos/' + photoId);
if (!response.ok) {
throw new Error('Failed to fetch photo ' + photoId + ': ' + response.statusText);
}
return await response.json();
} catch (error) {
console.error('Error fetching photo ' + photoId + ' metadata:', error);
return null;
}
}
/**
* Fetch album data from Media API
*/
async function fetchAlbumData(albumId) {
try {
var response = await fetch(MEDIA_API_URL + '/api/public/albums/' + albumId);
if (!response.ok) {
throw new Error('Failed to fetch album ' + albumId + ': ' + response.statusText);
}
return await response.json();
} catch (error) {
console.error('Error fetching album ' + albumId + ':', error);
return null;
}
}
// ─── Photo Block ─────────────────────────────────────────────────────
/**
* Render a single photo block.
* Replaces placeholder with an <img> tag, optional caption, and gallery link.
*/
async function renderPhotoBlock(el) {
var photoId = parseInt(el.dataset.photoId, 10);
var size = el.dataset.size || 'large';
var caption = el.dataset.caption || '';
var linkToGallery = el.dataset.linkToGallery !== 'false';
var alignment = el.dataset.alignment || 'center';
if (!photoId || isNaN(photoId)) {
showError(el, 'Invalid photo ID');
return;
}
// Show loading state
el.innerHTML = '<div class="photo-loading">Loading photo...</div>';
// Fetch metadata for alt text and title
var metadata = await fetchPhotoMetadata(photoId);
if (!metadata) {
showError(el, 'Photo not found (ID: ' + photoId + ')');
return;
}
var imgUrl = MEDIA_API_URL + '/api/public/photos/' + photoId + '/image?size=' + size;
var altText = escapeHtml(metadata.title || 'Photo ' + photoId);
// Build image element
var img = document.createElement('img');
img.src = imgUrl;
img.alt = altText;
img.title = altText;
img.style.width = '100%';
img.style.height = 'auto';
img.style.borderRadius = '8px';
img.style.display = 'block';
// Clear and rebuild
el.innerHTML = '';
el.style.textAlign = alignment;
if (linkToGallery) {
var link = document.createElement('a');
link.href = getGalleryUrl(photoId);
link.target = '_blank';
link.appendChild(img);
el.appendChild(link);
} else {
el.appendChild(img);
}
// Add caption if provided (from data attribute or metadata)
var captionText = caption || metadata.description || '';
if (captionText) {
var captionEl = document.createElement('div');
captionEl.className = 'photo-caption';
captionEl.textContent = captionText;
el.appendChild(captionEl);
}
console.log('Rendered photo block for photo ' + photoId);
}
// ─── Photo Card ──────────────────────────────────────────────────────
/**
* Hydrate a photo card block:
* 1. Fix thumbnail URL using MEDIA_API_URL
* 2. Fetch live metadata (view count)
* 3. Mark as hydrated
*/
async function hydratePhotoCard(cardEl) {
var photoId = parseInt(cardEl.dataset.photoId, 10);
if (!photoId || isNaN(photoId)) return;
// 1. Fix thumbnail URL
var img = cardEl.querySelector('img');
if (img) {
var correctThumbUrl = MEDIA_API_URL + '/api/public/photos/' + photoId + '/thumbnail?v=' + Date.now();
img.src = correctThumbUrl;
img.onerror = function() {
img.style.display = 'none';
};
}
// 2. Fetch live metadata to update views
try {
var metadata = await fetchPhotoMetadata(photoId);
if (metadata) {
// Update view count if rendered in card
var viewsEl = cardEl.querySelector('[data-role="views"]');
if (viewsEl && metadata.viewCount !== undefined) {
viewsEl.textContent = formatViewCount(metadata.viewCount);
}
}
} catch (e) {
// Non-critical
}
// 3. Fix "View →" link to use correct gallery URL
var link = cardEl.querySelector('a');
if (link) {
link.href = getGalleryUrl(photoId);
link.target = '_blank';
}
// 4. Replace "View →" text with gallery button
var infoBar = cardEl.querySelector('a > div:last-child');
if (infoBar) {
var flexRow = infoBar.querySelector('div:last-child');
if (flexRow) {
var spans = flexRow.querySelectorAll('span');
var viewSpan = null;
for (var i = 0; i < spans.length; i++) {
if (spans[i].textContent.indexOf('View') !== -1) {
viewSpan = spans[i];
break;
}
}
if (viewSpan) {
var galleryLink = document.createElement('a');
galleryLink.href = getGalleryUrl(photoId);
galleryLink.target = '_blank';
galleryLink.innerHTML = '&#128247; Gallery';
galleryLink.className = 'photo-card-gallery-btn';
galleryLink.style.cssText = 'background:#43cea2;border:none;color:#fff;font-size:12px;font-weight:600;padding:5px 14px;border-radius:4px;cursor:pointer;transition:background 0.15s;text-decoration:none;display:inline-block;';
galleryLink.title = 'Open in gallery';
galleryLink.onmouseenter = function() { galleryLink.style.background = '#5dd8b0'; };
galleryLink.onmouseleave = function() { galleryLink.style.background = '#43cea2'; };
galleryLink.addEventListener('click', function(ev) {
ev.stopPropagation();
});
viewSpan.replaceWith(galleryLink);
}
}
}
// Mark as hydrated
cardEl.dataset.hydrated = 'true';
console.log('Hydrated photo card for photo ' + photoId);
}
// ─── Photo Album ─────────────────────────────────────────────────────
/**
* Render a photo album block.
* Fetches album data and builds a responsive grid of thumbnails.
*/
async function renderAlbumBlock(el) {
var albumId = parseInt(el.dataset.albumId, 10);
var columns = parseInt(el.dataset.columns, 10) || 3;
var maxPhotos = parseInt(el.dataset.maxPhotos, 10) || 12;
var showTitle = el.dataset.showTitle !== 'false';
if (!albumId || isNaN(albumId)) {
showError(el, 'Invalid album ID');
return;
}
// Show loading state
el.innerHTML = '<div class="photo-loading">Loading album...</div>';
// Fetch album data
var album = await fetchAlbumData(albumId);
if (!album) {
showError(el, 'Album not found (ID: ' + albumId + ')');
return;
}
// Build album content
el.innerHTML = '';
// Album title
if (showTitle && album.title) {
var titleEl = document.createElement('h3');
titleEl.className = 'photo-album-title';
titleEl.textContent = album.title;
el.appendChild(titleEl);
}
// Photo grid
var photos = (album.photos || []).slice(0, maxPhotos);
if (photos.length === 0) {
el.innerHTML += '<div class="photo-loading">This album has no photos yet.</div>';
return;
}
var grid = document.createElement('div');
grid.className = 'photo-album-grid cols-' + columns;
for (var i = 0; i < photos.length; i++) {
var photo = photos[i];
var cell = document.createElement('div');
cell.className = 'photo-album-cell';
var cellLink = document.createElement('a');
cellLink.href = getGalleryUrl(photo.id);
cellLink.target = '_blank';
cellLink.title = photo.title || 'Photo ' + photo.id;
var cellImg = document.createElement('img');
cellImg.src = MEDIA_API_URL + '/api/public/photos/' + photo.id + '/thumbnail';
cellImg.alt = photo.title || 'Photo ' + photo.id;
cellImg.loading = 'lazy';
cellLink.appendChild(cellImg);
cell.appendChild(cellLink);
grid.appendChild(cell);
}
el.appendChild(grid);
// "View full album" footer
var footer = document.createElement('div');
footer.className = 'photo-album-footer';
var footerLink = document.createElement('a');
footerLink.href = PUBLIC_URL + '/gallery?album=' + albumId;
footerLink.target = '_blank';
footerLink.textContent = 'View full album \u2192';
footer.appendChild(footerLink);
el.appendChild(footer);
console.log('Rendered album block for album ' + albumId + ' (' + photos.length + ' photos)');
}
// ─── Photo Album Carousel ──────────────────────────────────────────
/**
* Render a photo album carousel.
* Fetches album data and builds a swipeable slideshow with navigation.
*/
async function renderCarouselBlock(el) {
var albumId = parseInt(el.dataset.albumId, 10);
var maxPhotos = parseInt(el.dataset.maxPhotos, 10) || 20;
var showTitle = el.dataset.showTitle !== 'false';
var autoPlayEnabled = el.dataset.autoPlay === 'true';
if (!albumId || isNaN(albumId)) {
showError(el, 'Invalid album ID');
return;
}
// Show loading state
el.innerHTML = '<div class="photo-loading">Loading carousel...</div>';
// Fetch album data
var album = await fetchAlbumData(albumId);
if (!album) {
showError(el, 'Album not found (ID: ' + albumId + ')');
return;
}
var photos = (album.photos || []).slice(0, maxPhotos);
if (photos.length === 0) {
el.innerHTML = '<div class="photo-loading">This album has no photos yet.</div>';
return;
}
// Build carousel
el.innerHTML = '';
el.classList.add('carousel-initialized');
// Album title
if (showTitle && album.title) {
var titleEl = document.createElement('h3');
titleEl.className = 'photo-album-title';
titleEl.textContent = album.title;
el.appendChild(titleEl);
}
// Viewport + track
var viewport = document.createElement('div');
viewport.className = 'carousel-viewport';
var track = document.createElement('div');
track.className = 'carousel-track';
for (var i = 0; i < photos.length; i++) {
var photo = photos[i];
var slide = document.createElement('div');
slide.className = 'carousel-slide';
var slideLink = document.createElement('a');
slideLink.href = getGalleryUrl(photo.id);
slideLink.target = '_blank';
slideLink.title = photo.title || 'Photo ' + photo.id;
var slideImg = document.createElement('img');
slideImg.src = MEDIA_API_URL + '/api/public/photos/' + photo.id + '/image?size=large';
slideImg.alt = photo.title || 'Photo ' + photo.id;
slideImg.loading = i === 0 ? 'eager' : 'lazy';
slideLink.appendChild(slideImg);
slide.appendChild(slideLink);
track.appendChild(slide);
}
viewport.appendChild(track);
el.appendChild(viewport);
// Navigation buttons (only if > 1 photo)
if (photos.length > 1) {
var prevBtn = document.createElement('button');
prevBtn.className = 'carousel-btn carousel-btn-prev';
prevBtn.setAttribute('aria-label', 'Previous photo');
prevBtn.innerHTML = '&#10094;';
viewport.appendChild(prevBtn);
var nextBtn = document.createElement('button');
nextBtn.className = 'carousel-btn carousel-btn-next';
nextBtn.setAttribute('aria-label', 'Next photo');
nextBtn.innerHTML = '&#10095;';
viewport.appendChild(nextBtn);
// Dot indicators
var dotsWrap = document.createElement('div');
dotsWrap.className = 'carousel-dots';
for (var d = 0; d < photos.length; d++) {
var dot = document.createElement('button');
dot.className = 'carousel-dot' + (d === 0 ? ' active' : '');
dot.setAttribute('aria-label', 'Go to photo ' + (d + 1));
dot.dataset.index = String(d);
dotsWrap.appendChild(dot);
}
el.appendChild(dotsWrap);
// Carousel state
var currentIndex = 0;
var totalSlides = photos.length;
var autoPlayTimer = null;
function goTo(idx) {
if (idx < 0) idx = totalSlides - 1;
if (idx >= totalSlides) idx = 0;
currentIndex = idx;
track.style.transform = 'translateX(-' + (currentIndex * 100) + '%)';
// Update dots
var dots = dotsWrap.querySelectorAll('.carousel-dot');
for (var j = 0; j < dots.length; j++) {
dots[j].classList.toggle('active', j === currentIndex);
}
}
prevBtn.addEventListener('click', function(e) {
e.preventDefault();
goTo(currentIndex - 1);
resetAutoPlay();
});
nextBtn.addEventListener('click', function(e) {
e.preventDefault();
goTo(currentIndex + 1);
resetAutoPlay();
});
dotsWrap.addEventListener('click', function(e) {
var target = e.target;
if (target.classList.contains('carousel-dot')) {
goTo(parseInt(target.dataset.index, 10));
resetAutoPlay();
}
});
// Keyboard navigation
el.setAttribute('tabindex', '0');
el.addEventListener('keydown', function(e) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
goTo(currentIndex - 1);
resetAutoPlay();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
goTo(currentIndex + 1);
resetAutoPlay();
}
});
// Touch/swipe support
var touchStartX = 0;
var touchDeltaX = 0;
viewport.addEventListener('touchstart', function(e) {
touchStartX = e.touches[0].clientX;
touchDeltaX = 0;
}, { passive: true });
viewport.addEventListener('touchmove', function(e) {
touchDeltaX = e.touches[0].clientX - touchStartX;
}, { passive: true });
viewport.addEventListener('touchend', function(e) {
if (Math.abs(touchDeltaX) > 50) {
e.preventDefault(); // suppress follow-up click from swipe
if (touchDeltaX < 0) goTo(currentIndex + 1);
else goTo(currentIndex - 1);
resetAutoPlay();
}
touchDeltaX = 0;
});
// Auto-play
function startAutoPlay() {
if (!autoPlayEnabled) return;
stopAutoPlay();
autoPlayTimer = setInterval(function() {
goTo(currentIndex + 1);
}, 5000);
}
function stopAutoPlay() {
if (autoPlayTimer) {
clearInterval(autoPlayTimer);
autoPlayTimer = null;
}
}
function resetAutoPlay() {
stopAutoPlay();
startAutoPlay();
}
// Pause auto-play on hover
el.addEventListener('mouseenter', stopAutoPlay);
el.addEventListener('mouseleave', startAutoPlay);
startAutoPlay();
}
// "View full album" footer
var footer = document.createElement('div');
footer.className = 'photo-album-footer';
var footerLink = document.createElement('a');
footerLink.href = PUBLIC_URL + '/gallery?album=' + albumId;
footerLink.target = '_blank';
footerLink.textContent = 'View full album \u2192';
footer.appendChild(footerLink);
el.appendChild(footer);
console.log('Rendered carousel for album ' + albumId + ' (' + photos.length + ' photos)');
}
// ─── Initialization ──────────────────────────────────────────────────
/**
* Initialize all photo carousel blocks on the page
*/
function initPhotoCarousels() {
var carousels = document.querySelectorAll('.photo-album-carousel');
console.log('Found ' + carousels.length + ' photo carousel(s) to hydrate');
carousels.forEach(function(carousel) {
if (carousel.classList.contains('carousel-initialized')) return; // Already rendered
renderCarouselBlock(carousel);
});
}
/**
* Initialize all photo blocks on the page
*/
function initPhotoBlocks() {
var blocks = document.querySelectorAll('.photo-block');
console.log('Found ' + blocks.length + ' photo block(s) to hydrate');
blocks.forEach(function(block) {
if (block.querySelector('img')) return; // Already rendered
renderPhotoBlock(block);
});
}
/**
* Initialize all photo card blocks on the page
*/
function initPhotoCards() {
var cards = document.querySelectorAll('.photo-card-block');
console.log('Found ' + cards.length + ' photo card(s) to hydrate');
cards.forEach(function(card) {
if (card.dataset.hydrated === 'true') return;
hydratePhotoCard(card);
});
}
/**
* Initialize all photo album blocks on the page
*/
function initPhotoAlbums() {
var albums = document.querySelectorAll('.photo-album-block');
console.log('Found ' + albums.length + ' photo album(s) to hydrate');
albums.forEach(function(album) {
if (album.querySelector('.photo-album-grid')) return; // Already rendered
renderAlbumBlock(album);
});
}
/**
* Initialize all photo elements (blocks + cards + albums + carousels)
*/
function initAll() {
initPhotoBlocks();
initPhotoCards();
initPhotoAlbums();
initPhotoCarousels();
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
// Re-initialize on MkDocs SPA navigation
if (typeof window.document$ !== 'undefined') {
window.document$.subscribe(function() {
console.log('MkDocs navigation detected, re-initializing photo elements');
setTimeout(initAll, 100);
});
}
})();

View File

@@ -7,10 +7,10 @@
"stars_count": 0,
"forks_count": 0,
"open_issues_count": 23,
"updated_at": "2026-02-18T10:02:02-07:00",
"updated_at": "2026-02-21T11:52:28-07:00",
"created_at": "2025-05-28T14:54:59-06:00",
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
"default_branch": "main",
"last_build_update": "2026-02-18T10:02:02-07:00"
"last_build_update": "2026-02-21T11:52:28-07:00"
}

View File

@@ -4,13 +4,13 @@
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
"html_url": "https://github.com/anthropics/claude-code",
"language": "Shell",
"stars_count": 67570,
"forks_count": 5278,
"open_issues_count": 6297,
"updated_at": "2026-02-18T18:41:22Z",
"stars_count": 70476,
"forks_count": 5568,
"open_issues_count": 6924,
"updated_at": "2026-02-26T14:25:29Z",
"created_at": "2025-02-22T17:41:21Z",
"clone_url": "https://github.com/anthropics/claude-code.git",
"ssh_url": "git@github.com:anthropics/claude-code.git",
"default_branch": "main",
"last_build_update": "2026-02-17T18:53:52Z"
"last_build_update": "2026-02-26T12:05:37Z"
}

View File

@@ -4,13 +4,13 @@
"description": "VS Code in the browser",
"html_url": "https://github.com/coder/code-server",
"language": "TypeScript",
"stars_count": 76286,
"forks_count": 6513,
"open_issues_count": 177,
"updated_at": "2026-02-18T17:58:26Z",
"stars_count": 76383,
"forks_count": 6527,
"open_issues_count": 184,
"updated_at": "2026-02-26T13:37:54Z",
"created_at": "2019-02-27T16:50:41Z",
"clone_url": "https://github.com/coder/code-server.git",
"ssh_url": "git@github.com:coder/code-server.git",
"default_branch": "main",
"last_build_update": "2026-02-14T12:44:57Z"
"last_build_update": "2026-02-25T22:36:16Z"
}

View File

@@ -4,13 +4,13 @@
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
"html_url": "https://github.com/gethomepage/homepage",
"language": "JavaScript",
"stars_count": 28459,
"forks_count": 1789,
"stars_count": 28611,
"forks_count": 1795,
"open_issues_count": 1,
"updated_at": "2026-02-18T17:08:54Z",
"updated_at": "2026-02-26T12:19:48Z",
"created_at": "2022-08-24T07:29:42Z",
"clone_url": "https://github.com/gethomepage/homepage.git",
"ssh_url": "git@github.com:gethomepage/homepage.git",
"default_branch": "dev",
"last_build_update": "2026-02-18T12:23:05Z"
"last_build_update": "2026-02-26T12:25:33Z"
}

View File

@@ -4,13 +4,13 @@
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
"html_url": "https://github.com/go-gitea/gitea",
"language": "Go",
"stars_count": 53764,
"forks_count": 6394,
"open_issues_count": 2832,
"updated_at": "2026-02-18T18:54:21Z",
"stars_count": 53910,
"forks_count": 6399,
"open_issues_count": 2843,
"updated_at": "2026-02-26T13:59:20Z",
"created_at": "2016-11-01T02:13:26Z",
"clone_url": "https://github.com/go-gitea/gitea.git",
"ssh_url": "git@github.com:go-gitea/gitea.git",
"default_branch": "main",
"last_build_update": "2026-02-18T07:31:28Z"
"last_build_update": "2026-02-26T11:57:03Z"
}

View File

@@ -4,13 +4,13 @@
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
"html_url": "https://github.com/knadh/listmonk",
"language": "Go",
"stars_count": 19078,
"forks_count": 1928,
"stars_count": 19131,
"forks_count": 1936,
"open_issues_count": 113,
"updated_at": "2026-02-18T10:42:44Z",
"updated_at": "2026-02-26T12:29:54Z",
"created_at": "2019-06-26T05:08:39Z",
"clone_url": "https://github.com/knadh/listmonk.git",
"ssh_url": "git@github.com:knadh/listmonk.git",
"default_branch": "master",
"last_build_update": "2026-02-17T05:10:24Z"
"last_build_update": "2026-02-26T13:26:07Z"
}

View File

@@ -4,10 +4,10 @@
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
"html_url": "https://github.com/lyqht/mini-qr",
"language": "Vue",
"stars_count": 1852,
"forks_count": 236,
"open_issues_count": 21,
"updated_at": "2026-02-17T22:05:48Z",
"stars_count": 1878,
"forks_count": 240,
"open_issues_count": 24,
"updated_at": "2026-02-26T06:04:32Z",
"created_at": "2023-04-21T14:20:14Z",
"clone_url": "https://github.com/lyqht/mini-qr.git",
"ssh_url": "git@github.com:lyqht/mini-qr.git",

View File

@@ -4,13 +4,13 @@
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
"html_url": "https://github.com/n8n-io/n8n",
"language": "TypeScript",
"stars_count": 175133,
"forks_count": 54959,
"open_issues_count": 1379,
"updated_at": "2026-02-18T18:44:06Z",
"stars_count": 176496,
"forks_count": 55212,
"open_issues_count": 1408,
"updated_at": "2026-02-26T14:25:19Z",
"created_at": "2019-06-22T09:24:21Z",
"clone_url": "https://github.com/n8n-io/n8n.git",
"ssh_url": "git@github.com:n8n-io/n8n.git",
"default_branch": "master",
"last_build_update": "2026-02-18T18:45:01Z"
"last_build_update": "2026-02-26T14:25:24Z"
}

View File

@@ -4,13 +4,13 @@
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
"html_url": "https://github.com/nocodb/nocodb",
"language": "TypeScript",
"stars_count": 62055,
"forks_count": 4637,
"open_issues_count": 595,
"updated_at": "2026-02-18T18:47:12Z",
"stars_count": 62217,
"forks_count": 4647,
"open_issues_count": 602,
"updated_at": "2026-02-25T22:03:40Z",
"created_at": "2017-10-29T18:51:48Z",
"clone_url": "https://github.com/nocodb/nocodb.git",
"ssh_url": "git@github.com:nocodb/nocodb.git",
"default_branch": "develop",
"last_build_update": "2026-02-18T17:46:29Z"
"last_build_update": "2026-02-25T19:01:33Z"
}

View File

@@ -4,13 +4,13 @@
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
"html_url": "https://github.com/ollama/ollama",
"language": "Go",
"stars_count": 162853,
"forks_count": 14611,
"open_issues_count": 2427,
"updated_at": "2026-02-18T18:55:27Z",
"stars_count": 163469,
"forks_count": 14678,
"open_issues_count": 2486,
"updated_at": "2026-02-26T14:21:37Z",
"created_at": "2023-06-26T19:39:32Z",
"clone_url": "https://github.com/ollama/ollama.git",
"ssh_url": "git@github.com:ollama/ollama.git",
"default_branch": "main",
"last_build_update": "2026-02-18T02:18:06Z"
"last_build_update": "2026-02-26T03:28:54Z"
}

View File

@@ -4,13 +4,13 @@
"description": "Documentation that simply works",
"html_url": "https://github.com/squidfunk/mkdocs-material",
"language": "Python",
"stars_count": 26074,
"forks_count": 4044,
"stars_count": 26122,
"forks_count": 4047,
"open_issues_count": 2,
"updated_at": "2026-02-18T16:48:45Z",
"updated_at": "2026-02-26T12:52:29Z",
"created_at": "2016-01-28T22:09:23Z",
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
"default_branch": "master",
"last_build_update": "2026-02-18T15:52:24Z"
"last_build_update": "2026-02-24T12:06:19Z"
}