feat: add story.html.twig with hero scroll effect and shortcode JS

This commit is contained in:
2026-06-19 22:56:00 +02:00
parent fcdb3de387
commit 0dc9095b4b
@@ -0,0 +1,197 @@
{% extends 'partials/base.html.twig' %}
{% block nav %}
<a class="story-escape" href="{{ page.parent().url }}">← Stories</a>
{% endblock %}
{% block content %}
{% set hero = null %}
{% if page.header.hero_image and page.media[page.header.hero_image] is defined %}
{% set hero = page.media[page.header.hero_image] %}
{% endif %}
{% set date_str = page.date|date('d M Y') %}
{% if page.header.end_date %}
{% set end_str = page.header.end_date|date('d M Y') %}
{% set date_str = page.date|date('d M') ~ '' ~ end_str %}
{% endif %}
{% set location = page.header.location_name ?? '' %}
<div class="story-hero" id="story-hero">
<div class="story-hero__img-wrap">
{% if hero %}
<img src="{{ hero.url }}" alt="{{ page.header.hero_alt ?? page.title }}" class="story-hero__img" loading="eager">
{% else %}
<div class="story-hero__img-placeholder"></div>
{% endif %}
</div>
<div class="story-hero__overlay" id="story-overlay" aria-hidden="true"></div>
<div class="story-hero__content">
<h1 class="story-hero__title">{{ page.title }}</h1>
<p class="story-hero__meta">
<time datetime="{{ page.date|date('Y-m-d') }}">{{ date_str }}</time>
{% if location %} · {{ location }}{% endif %}
</p>
</div>
<div class="story-hero__scroll-cue" id="story-scroll-cue" aria-hidden="true">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div class="story-hero__spacer"></div>
</div>
<div class="story-body">
{{ page.content|raw }}
<footer class="story-footer">
<a href="{{ page.parent().url }}">← Back to stories</a>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/scrollama@3/build/scrollama.min.js"></script>
<script>
/* ── Hero scroll effect ──────────────────────────────────── */
(function () {
var overlay = document.getElementById('story-overlay');
var cue = document.getElementById('story-scroll-cue');
var hidden = false;
var ticking = false;
if (!overlay) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
function update() {
var progress = Math.min(window.scrollY / window.innerHeight, 1);
overlay.style.background = 'rgba(0,0,0,' + (progress * 0.65).toFixed(3) + ')';
if (progress >= 1) overlay.style.display = 'none';
else overlay.style.display = '';
if (!hidden && window.scrollY > 80) {
cue.classList.add('is-hidden');
hidden = true;
}
ticking = false;
}
window.addEventListener('scroll', function () {
if (!ticking) { requestAnimationFrame(update); ticking = true; }
}, { passive: true });
update();
})();
/* ── ChapterBreak scroll-reveal ─────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var panels = document.querySelectorAll('.chapter-break__panel');
if (!panels.length) return;
if (reduced) { panels.forEach(function (p) { p.classList.add('is-revealed'); }); return; }
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
});
}, { threshold: 0.25 });
panels.forEach(function (p) { io.observe(p); });
})();
/* ── PullQuote scroll-reveal ─────────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var quotes = document.querySelectorAll('.pull-quote');
if (!quotes.length) return;
if (reduced) { quotes.forEach(function (q) { q.classList.add('is-revealed'); }); return; }
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
});
}, { threshold: 0.2 });
quotes.forEach(function (q) { io.observe(q); });
})();
/* ── SnapGallery dot indicator ───────────────────────────── */
(function () {
document.querySelectorAll('.pgallery').forEach(function (gallery) {
var slides = gallery.querySelectorAll('.pgallery__slide');
var dots = gallery.querySelectorAll('.pgallery__dot');
if (!slides.length || !dots.length) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
var idx = parseInt(e.target.dataset.index, 10);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[idx]) dots[idx].classList.add('is-active');
}
});
}, { threshold: 0.5, root: gallery.querySelector('.pgallery__frame') });
slides.forEach(function (s) { io.observe(s); });
});
})();
/* ── ScrollySection (Scrollama) ──────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var sections = document.querySelectorAll('.scrolly');
if (!sections.length || typeof scrollama === 'undefined') return;
var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%'];
sections.forEach(function (section) {
var img = section.querySelector('.scrolly__img');
var overlay = section.querySelector('.scrolly__img-overlay');
var contentEl = section.querySelector('.scrolly__steps-content');
var stepsWrap = section.querySelector('.scrolly__steps');
if (!img || !contentEl || !stepsWrap) return;
/* Split slot content on <hr> into individual step divs */
var nodes = Array.from(contentEl.childNodes);
var groups = [];
var curr = [];
nodes.forEach(function (n) {
if (n.nodeName === 'HR') { if (curr.length) groups.push(curr); curr = []; }
else curr.push(n);
});
if (curr.length) groups.push(curr);
if (!groups.length) groups.push(nodes);
groups.forEach(function (group) {
var step = document.createElement('div');
step.className = 'scrolly-step';
var inner = document.createElement('div');
inner.className = 'scrolly-step__inner';
group.forEach(function (n) { inner.appendChild(n.cloneNode(true)); });
step.appendChild(inner);
stepsWrap.appendChild(step);
});
if (reduced) {
section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); });
return;
}
/* Initial blur */
img.style.filter = 'blur(8px)';
img.style.transform = 'scale(1.04)';
img.style.transition = 'filter 0.7s cubic-bezier(.16,1,.3,1), transform 0.7s cubic-bezier(.16,1,.3,1), object-position 1.2s cubic-bezier(.16,1,.3,1)';
new IntersectionObserver(function (entries, obs) {
if (entries[0].isIntersecting) {
img.style.filter = 'blur(0)';
img.style.transform = 'scale(1)';
obs.disconnect();
}
}, { threshold: 0.1 }).observe(section);
scrollama()
.setup({ step: Array.from(section.querySelectorAll('.scrolly-step')), offset: 0.55 })
.onStepEnter(function (d) {
d.element.classList.add('is-active');
img.style.objectPosition = panOffsets[d.index % panOffsets.length];
if (overlay) overlay.style.background = 'rgba(0,0,0,' + (0.05 + (d.index % 3) * 0.05) + ')';
})
.onStepExit(function (d) {
if (d.direction === 'up') d.element.classList.remove('is-active');
});
});
})();
</script>
{% endblock %}