feat: sticky locked-image thumbnail while scrolling; two-click preview-then-open on the portfolio index
the locked image now gets a small pinned copy (18rem, same width as the text column) that appears once the real preview scrolls out of view and disappears again once the text runs out — fixed-position + scroll-driven rather than CSS sticky, since the filebrowser box is only ever as tall as its own content, not the much taller text next to it.\n\nalso scoped the existing click-outside-unfreezes-the-lock listener to clicks inside .filebrowser only, since with listings now starting locked, a click to read or select a word in the text would otherwise silently kill the sticky preview.\n\nportfolio index: rows there are real links (href + data-img together), so a single click used to jump straight into the project — the only way to preview a different one first was hovering, which does nothing for touch and is now also blocked while something is locked. first click on a different project now just previews it (like hover used to); a second click on the same row, or clicking the preview image itself, opens it.
This commit is contained in:
@@ -172,6 +172,10 @@ body:not(.home) .menu { display: none; }
|
||||
display: block;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
/* portfolio index only (not project pages): the big preview is itself a
|
||||
second way into the already-previewed project, next to clicking its row
|
||||
again */
|
||||
.filebrowser:not(.tui--files) .tui__preview img { cursor: pointer; }
|
||||
|
||||
/* project page: image files (no date column), show full image (contain) */
|
||||
.tui--files .tui__ls a { grid-template-columns: 11ch 1fr; }
|
||||
@@ -247,6 +251,38 @@ body:not(.home) .menu { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
/* small pinned copy of the locked image — appears at the left edge (same
|
||||
spot the real preview normally sits) once that preview has scrolled out
|
||||
of view, and disappears again once the text next to it runs out. width
|
||||
matches .proj-info so the two columns read as a pair. fixed, not sticky:
|
||||
the file list's own box is only as tall as its own content, not the much
|
||||
taller text next to it, so sticky positioning has nothing to be bounded
|
||||
by — a scroll-driven fixed element sidesteps that entirely. */
|
||||
.proj-sticky {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: calc((100vw - var(--container-width)) / 2);
|
||||
transform: translateY(-50%);
|
||||
width: 18rem;
|
||||
z-index: 5;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.proj-sticky.is-visible { opacity: 1; visibility: visible; }
|
||||
.proj-sticky img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 1099.98px) {
|
||||
.proj-sticky { display: none; }
|
||||
}
|
||||
|
||||
/* prev/next as shell "cd" commands — same vocabulary as the rest of the site */
|
||||
.proj-nav {
|
||||
font-family: var(--font-family-mono);
|
||||
|
||||
@@ -4,12 +4,54 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
var rows = [], img = null, texts = [], cap = null, cur = 0, frozen = false, container = null, player = null, pv = null;
|
||||
var layout = null, mainPreview = null, sticky = null, stickyImg = null;
|
||||
|
||||
function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); }
|
||||
function showText(k) { texts.forEach(function (t) { t.hidden = (t.getAttribute("data-text") !== k); }); }
|
||||
function hidePlayer() { if (player) { player.hidden = true; if (pv && !pv.paused) pv.pause(); } }
|
||||
function showPlayer() { if (player) player.hidden = false; }
|
||||
function setFrozen(v) { frozen = v; if (container) container.classList.toggle("is-frozen", v); }
|
||||
function setFrozen(v) { frozen = v; if (container) container.classList.toggle("is-frozen", v); syncSticky(); }
|
||||
|
||||
// small pinned copy of the locked image, shown to the left of the text
|
||||
// column once the real preview has scrolled out of view — mirrors the
|
||||
// LOCKED row only (like the main preview already does once frozen), never
|
||||
// the hover state, and only for as long as there's still text alongside it.
|
||||
function ensureSticky() {
|
||||
if (sticky) return;
|
||||
sticky = document.createElement("div");
|
||||
sticky.className = "proj-sticky";
|
||||
stickyImg = document.createElement("img");
|
||||
stickyImg.alt = "";
|
||||
sticky.appendChild(stickyImg);
|
||||
document.body.appendChild(sticky);
|
||||
}
|
||||
function syncSticky() {
|
||||
if (!sticky) return;
|
||||
var a = frozen ? rows[cur] : null;
|
||||
var src = a ? a.getAttribute("data-img") : null;
|
||||
if (src && layout && mainPreview) {
|
||||
if (stickyImg.getAttribute("src") !== src) stickyImg.src = src;
|
||||
updateStickyVisibility();
|
||||
} else {
|
||||
sticky.classList.remove("is-visible");
|
||||
}
|
||||
}
|
||||
function updateStickyVisibility() {
|
||||
if (!sticky || !stickyImg.getAttribute("src") || !layout || !mainPreview) return;
|
||||
if (!window.matchMedia("(min-width: 1100px)").matches) { sticky.classList.remove("is-visible"); return; }
|
||||
var pr = mainPreview.getBoundingClientRect();
|
||||
var lr = layout.getBoundingClientRect();
|
||||
var show = pr.bottom < 0 && lr.bottom > 0;
|
||||
sticky.classList.toggle("is-visible", show);
|
||||
}
|
||||
var stickyTick = false;
|
||||
function onScrollOrResize() {
|
||||
if (stickyTick) return;
|
||||
stickyTick = true;
|
||||
requestAnimationFrame(function () { stickyTick = false; updateStickyVisibility(); });
|
||||
}
|
||||
window.addEventListener("scroll", onScrollOrResize, { passive: true });
|
||||
window.addEventListener("resize", onScrollOrResize);
|
||||
|
||||
// tell the shell to "type" the matching command. navigable rows (those with an
|
||||
// href: ".." and the portfolio project folders) are directories → `cd`; the
|
||||
@@ -71,18 +113,47 @@
|
||||
pv = player ? player.querySelector("video") : null;
|
||||
cap = document.querySelector(".tui__status");
|
||||
cur = 0; frozen = false;
|
||||
if (!rows.length) return;
|
||||
|
||||
layout = document.querySelector(".proj-layout");
|
||||
mainPreview = document.querySelector(".filebrowser .tui__preview");
|
||||
ensureSticky();
|
||||
sticky.classList.remove("is-visible");
|
||||
if (!rows.length) { layout = null; return; }
|
||||
|
||||
rows.forEach(function (a, i) {
|
||||
// portfolio-index rows are real links (data-img + href together) — a
|
||||
// single click there used to jump straight into the project, which
|
||||
// meant hovering was the only way to preview a different one first.
|
||||
// now the first click just previews it (same as the old hover did);
|
||||
// only a second click on the already-previewed row, or the preview
|
||||
// image itself, actually navigates.
|
||||
var isProjectLink = a.hasAttribute("href") && a.hasAttribute("data-img");
|
||||
a.addEventListener("mouseenter", function () { if (!frozen) select(i); });
|
||||
a.addEventListener("click", function (e) {
|
||||
if (a.getAttribute("href")) return; // navigation rows behave normally
|
||||
if (isProjectLink) {
|
||||
if (frozen && cur === i) return; // already previewed — let the link through
|
||||
e.preventDefault();
|
||||
select(i);
|
||||
setFrozen(true);
|
||||
return;
|
||||
}
|
||||
if (a.getAttribute("href")) return; // ".." navigates normally
|
||||
e.preventDefault();
|
||||
if (frozen && cur === i) { setFrozen(false); ghostClear(); }
|
||||
else { select(i); setFrozen(true); }
|
||||
});
|
||||
});
|
||||
|
||||
if (img) {
|
||||
img.onclick = function () {
|
||||
var a = rows[cur];
|
||||
if (a && a.hasAttribute("href") && a.hasAttribute("data-img")) {
|
||||
var href = a.getAttribute("href");
|
||||
if (window.pjax) window.pjax(href); else window.location.href = href;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var start = rows.findIndex(function (a) { return a.classList.contains("is-selected"); });
|
||||
select(start < 0 ? 0 : start, true); // initial selection: don't echo into the shell yet
|
||||
// portfolio/about listings start pre-locked on their first file, same as a
|
||||
@@ -98,8 +169,13 @@
|
||||
}
|
||||
|
||||
// one-time document listeners (reference the current bound state)
|
||||
// clicking elsewhere IN THE WIDGET (the preview image, blank space in the
|
||||
// list) releases the lock; clicks out in the text/page don't — now that
|
||||
// listings start locked, a click to read or select a word shouldn't kill
|
||||
// the sticky preview underneath it.
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest && e.target.closest(".tui__ls a")) return;
|
||||
if (!(e.target.closest && e.target.closest(".filebrowser"))) return;
|
||||
if (frozen) { setFrozen(false); ghostClear(); }
|
||||
});
|
||||
document.addEventListener("keydown", function (e) {
|
||||
|
||||
Reference in New Issue
Block a user