Files
website/app/templates/layout.templ
Jimmy 3f5235daaf Improve gallery video UX and add upload-to-publish media workflow.
Stage raw files in upload/, publish with make sync-media/publish, and polish the lightbox: autoplay, remembered volume, Escape to close, and image/video icons without poster or caption clutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 23:55:43 +12:00

134 lines
4.0 KiB
Plaintext

package templates
templ Layout(title string, preloadImage string, content templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta name="theme-color" content="#14100c"/>
<title>{ title }</title>
<meta name="description" content="Technical Kiwi Limited — electronic engineering and interactive art from New Zealand."/>
<link rel="stylesheet" href="/static/style.css"/>
if preloadImage != "" {
<link rel="preload" as="image" href={ preloadImage } fetchpriority="high"/>
}
</head>
<body hx-boost="true">
<header class="site-header">
<a href="/" class="logo">
<span class="logo-mark" aria-hidden="true">T</span>
<span class="logo-text">Technical Kiwi</span>
</a>
<nav class="site-nav">
<a href="#services">Services</a>
<a href="https://git.technical.kiwi/" rel="noopener noreferrer" target="_blank">Code</a>
<a href="#contact">Contact</a>
</nav>
</header>
<main>
@content
</main>
<footer class="site-footer">
<p>&copy; { currentYear() } Technical Kiwi Limited. New Zealand.</p>
</footer>
<div id="modal-root"></div>
<script src="/static/htmx.min.js" defer></script>
<script>
(function () {
function hydrateLazyThumbs(root) {
var lazyImages = root.querySelectorAll("img[data-src]");
if (!lazyImages.length) return;
if (!("IntersectionObserver" in window)) {
lazyImages.forEach(function (img) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
});
return;
}
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
var img = entry.target;
img.src = img.dataset.src;
img.removeAttribute("data-src");
observer.unobserve(img);
});
},
{ rootMargin: "300px 0px" },
);
lazyImages.forEach(function (img) {
observer.observe(img);
});
}
document.addEventListener("DOMContentLoaded", function () {
hydrateLazyThumbs(document);
var modalRoot = document.getElementById("modal-root");
if (!modalRoot) return;
modalRoot.addEventListener("volumechange", function (event) {
if (event.target.tagName !== "VIDEO") return;
saveVideoVolume(event.target.volume);
});
});
function closeModal() {
var modal = document.getElementById("modal-root");
if (modal) modal.innerHTML = "";
}
var galleryVideoVolumeKey = "gallery-video-volume";
function getSavedVideoVolume() {
try {
var stored = localStorage.getItem(galleryVideoVolumeKey);
if (stored === null) return null;
var v = parseFloat(stored);
if (!isFinite(v) || v < 0 || v > 1) return null;
return v;
} catch (e) {
return null;
}
}
function saveVideoVolume(volume) {
try {
localStorage.setItem(galleryVideoVolumeKey, String(volume));
} catch (e) {}
}
function playModalVideo() {
var video = document.querySelector("#modal-root video");
if (!video) return;
var saved = getSavedVideoVolume();
if (saved !== null) video.volume = saved;
var p = video.play();
if (p && typeof p.catch === "function") {
p.catch(function () {});
}
}
document.addEventListener("keydown", function (event) {
if (event.key !== "Escape") return;
var modal = document.getElementById("modal-root");
if (!modal || !modal.firstElementChild) return;
event.preventDefault();
closeModal();
});
document.body.addEventListener("htmx:afterSwap", function (event) {
hydrateLazyThumbs(event.target);
if (event.detail && event.detail.target && event.detail.target.id === "modal-root") {
playModalVideo();
}
});
})();
</script>
</body>
</html>
}