feat: scroll-position memory on repertoire HTMX swaps + swap transition polish

- Add repertoire-scroll-restore.js: snapshots scrollTop of each column <ul>
  before htmx:beforeSwap, restores after htmx:afterSwap (keyed by data-col)
- Add subtle opacity transition on #repertoire-index during htmx-swapping
- Tighten rep-indicator opacity transition to 0.1s for snappier feedback
- Import new module in public-entry.js
This commit is contained in:
Pontoporeia
2026-06-24 14:45:51 +02:00
parent e0cf9f8f57
commit eb706214ce
4 changed files with 90 additions and 9 deletions

View File

@@ -290,6 +290,17 @@
color: var(--accent-primary);
}
/* ── Swap transition ────────────────────────────────────────────────── */
.repertoire-index.htmx-swapping {
opacity: 0.5;
transition: opacity 0.1s ease-out;
}
.repertoire-index {
transition: opacity 0.15s ease-in;
}
/* Link variant (students col) — no underline by default */
.rep-entry--link {
text-decoration: none;
@@ -333,7 +344,7 @@
height: 2px;
background: var(--accent-primary);
opacity: 0;
transition: opacity 0.15s;
transition: opacity 0.1s;
z-index: 100;
pointer-events: none;
}

View File

@@ -14,6 +14,7 @@ import "./htmx-global-setup.js";
// Public page features
import "./repertoire-accordion.js";
import "./repertoire-scroll-restore.js";
import "./repertoire-student-popover.js";
import "./access-request.js";
import "./acces-password.js";

View File

@@ -0,0 +1,70 @@
/**
* repertoire-scroll-restore.js — Preserve column scroll positions on HTMX swap.
*
* When a filter button triggers an HTMX swap that replaces #repertoire-index,
* the new markup replaces the old, resetting all column scroll positions to 0.
* This module captures scrollTop of each scrollable <ul> before the swap and
* restores it after, keyed by data-col attribute so the mapping survives
* DOM replacement.
*/
(() => {
var INDEX_SEL = '#repertoire-index';
var scrollSnapshots = {};
function snapshot() {
var index = document.querySelector(INDEX_SEL);
if (!index) return;
scrollSnapshots = {};
index.querySelectorAll('.repertoire-col[data-col] > ul').forEach((ul) => {
var col = ul.closest('.repertoire-col');
if (!col) return;
var key = col.getAttribute('data-col');
scrollSnapshots[key] = ul.scrollTop;
});
}
function restore() {
var index = document.querySelector(INDEX_SEL);
if (!index) return;
index.querySelectorAll('.repertoire-col[data-col] > ul').forEach((ul) => {
var col = ul.closest('.repertoire-col');
if (!col) return;
var key = col.getAttribute('data-col');
if (scrollSnapshots[key] !== undefined) {
ul.scrollTop = scrollSnapshots[key];
}
});
}
// Capture scroll positions before the outgoing element is replaced
document.body.addEventListener('htmx:beforeSwap', (e) => {
if (
e.detail.target &&
e.detail.target.matches &&
e.detail.target.matches(INDEX_SEL)
) {
snapshot();
}
});
// Restore after the new content is in the DOM
document.body.addEventListener('htmx:afterSwap', (e) => {
if (
e.detail.target &&
e.detail.target.matches &&
e.detail.target.matches(INDEX_SEL)
) {
// Use requestAnimationFrame to ensure layout has settled
requestAnimationFrame(() => {
restore();
});
}
});
// Also restore on history navigation (browser back/forward)
document.body.addEventListener('htmx:historyRestore', () => {
requestAnimationFrame(() => {
restore();
});
});
})();