Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dependency_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) {
"views/_tailwind_stylesheets.tmpl.html",
"views/_twitter.tmpl.html",
"views/_dark_mode_js.tmpl.html",
"views/_scroll_gradient_js.tmpl.html",
"views/_analytics_js.tmpl.html",
"views/_shiki_js.tmpl.html",
}, dependencies)
Expand All @@ -34,6 +35,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) {
"views/_tailwind_stylesheets.tmpl.html",
"views/_twitter.tmpl.html",
"views/_dark_mode_js.tmpl.html",
"views/_scroll_gradient_js.tmpl.html",
"views/_analytics_js.tmpl.html",
"views/_shiki_js.tmpl.html",
}, dependencies)
Expand All @@ -49,6 +51,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) {
"views/_tailwind_stylesheets.tmpl.html",
"views/_twitter.tmpl.html",
"views/_dark_mode_js.tmpl.html",
"views/_scroll_gradient_js.tmpl.html",
"views/_analytics_js.tmpl.html",
"views/_shiki_js.tmpl.html",
}, dependencies)
Expand Down
1 change: 1 addition & 0 deletions layouts/main.tmpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<body class="antialiased {{block "body_style" .}}{{end}}">
<!-- load early to avoid ficker -->
{{template "views/_dark_mode_js.tmpl.html" .}}
{{template "views/_scroll_gradient_js.tmpl.html" .}}

{{block "content" .}}{{end}}

Expand Down
262 changes: 262 additions & 0 deletions views/_scroll_gradient_js.tmpl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
<style>
/* Fixed overlay that sits behind all page content */
.scroll-gradient-bg {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
contain: paint;
}

/* Inner container that moves with scroll to create the gradient effect */
.scroll-gradient-bg__inner {
width: 100%;
height: 100%;
will-change: transform;
}

/* Lift all page content above the overlay without needing a wrapper */
.has-scroll-gradient> :not(.scroll-gradient-bg) {
position: relative;
z-index: 1;
}
</style>

<script>
// Because this fix needs to run inline, immediately after <body>, and in a
// specific order with other template-driven UI bits (like dark mode), I made
// it a small template partial instead of a separate JS asset. That lets it
// execute early enough to avoid flicker/double-paint, guarantees ordering,
// and avoids an extra network request for a tiny, critical rendering path
// script.

(function () {
try {
// Early exit if page isn't ready for DOM work
if (!document.body) return;

var body = document.body;
// Check if browser supports CSS containment for better performance
var supportsContain =
(window.CSS && CSS.supports && (CSS.supports('contain: paint') || CSS.supports('contain: strict'))) || false;

// DOM elements and state variables (kept in closure so we can cleanup and re-init)
var container = null;
var inner = null;
var rafId = null;
var lastScrollY = -1;
var resizeObserver = null;
var mo = null;

// Track original body background to restore later
var bodyInlineBackgroundImageOriginal = '';
var bodyBackgroundOverridden = false;

/**
* Copy computed background styles from body to the gradient overlay
* This ensures the gradient matches the current theme/class state
*/
function copyComputedBackground(target) {
var restoreInlineBackgroundImage = null;
if (bodyBackgroundOverridden) {
restoreInlineBackgroundImage = body.style.backgroundImage;
body.style.backgroundImage = bodyInlineBackgroundImageOriginal || '';
}

var cs = getComputedStyle(body);

// Copy all background properties to container
if (target === container && container) {
container.style.backgroundColor = cs.backgroundColor;
container.style.backgroundImage = cs.backgroundImage;
container.style.backgroundRepeat = cs.backgroundRepeat;
container.style.backgroundSize = cs.backgroundSize;
container.style.backgroundPosition = cs.backgroundPosition;
}
// Copy background properties to inner element
else if (target === inner && inner) {
container.style.backgroundColor = cs.backgroundColor;
inner.style.backgroundImage = cs.backgroundImage;
inner.style.backgroundRepeat = cs.backgroundRepeat;
inner.style.backgroundSize = cs.backgroundSize;
inner.style.backgroundPosition = cs.backgroundPosition;
}

// Restore any inline background that was temporarily removed
if (restoreInlineBackgroundImage !== null) {
body.style.backgroundImage = restoreInlineBackgroundImage;
}
}

/**
* Set the height of the inner gradient element to cover the full document
* This ensures the gradient extends beyond the viewport for smooth scrolling
*/
function setHeight() {
if (!inner) return;
var doc = document.documentElement;
var docHeight = Math.max(
doc.scrollHeight,
body.scrollHeight,
doc.offsetHeight,
body.offsetHeight
);
// Add 4px buffer to prevent any gaps during scroll
inner.style.height = (docHeight + 4) + 'px';
}

/**
* Handle scroll events by updating the gradient position
* Uses requestAnimationFrame for smooth performance
*/
function onScroll() {
if (!inner) return;
// If rAF is not available or not a function (e.g. monkey patched),
// fall back to synchronous update to avoid errors.
if (typeof window.requestAnimationFrame !== 'function') {
var dprSync = window.devicePixelRatio || 1;
var ySync = Math.round(window.scrollY * dprSync) / dprSync;
if (ySync === lastScrollY) return;
lastScrollY = ySync;
inner.style.transform = 'translateY(' + (-ySync) + 'px)';
return;
}
if (rafId != null) return;
rafId = window.requestAnimationFrame(function () {
rafId = null;
var dpr = window.devicePixelRatio || 1;
var y = Math.round(window.scrollY * dpr) / dpr;
if (y === lastScrollY) return;
lastScrollY = y;
inner.style.transform = 'translateY(' + (-y) + 'px)';
});
}

/**
* Handle resize events by recalculating height and scroll position
*/
function onResize() {
setHeight();
onScroll();
}

/**
* Install a mutation observer to watch for class changes on the document
* and body elements. When a class change is detected, we copy the computed
* background styles to the gradient overlay.
*/
function installMutationObserver() {
if (mo) mo.disconnect();
mo = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'class') {
copyComputedBackground(inner ? inner : container);
break;
}
}
});
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
mo.observe(body, { attributes: true, attributeFilter: ['class'] });
}

/**
* Cleanup the scroll gradient effect.
*/
function cleanup() {
try {
if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] cleanup pagehide');
if (rafId != null && typeof window.cancelAnimationFrame === 'function') window.cancelAnimationFrame(rafId);
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
if (resizeObserver && typeof resizeObserver.disconnect === 'function') resizeObserver.disconnect();
resizeObserver = null;
if (container && container.parentNode) container.parentNode.removeChild(container);
container = null;
inner = null;
if (bodyBackgroundOverridden) body.style.backgroundImage = bodyInlineBackgroundImageOriginal || '';
bodyBackgroundOverridden = false;
body.classList.remove('has-scroll-gradient');
if (mo) mo.disconnect();
window.__sorgScrollGradientInitialized = false;
} catch (_) {}
}

/**
* Initialize the scroll gradient effect.
*/
function initialize() {
// Only run on pages that have the gradient background class
if (!body.classList.contains('bg-gradient-to-br')) return;
// Prevent multiple initializations
if (window.__sorgScrollGradientInitialized) return;
window.__sorgScrollGradientInitialized = true;
if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] initialize');

// Remove any stale overlays if present (safety net)
var stale = document.querySelectorAll('.scroll-gradient-bg');
for (var i = 0; i < stale.length; i++) {
if (stale[i] && stale[i].parentNode) stale[i].parentNode.removeChild(stale[i]);
}

installMutationObserver();

if (supportsContain) {
// Create the gradient overlay container
container = document.createElement('div');
container.className = 'scroll-gradient-bg';

// Create the inner element that will move with scroll
inner = document.createElement('div');
inner.className = 'scroll-gradient-bg__inner';
container.appendChild(inner);

// Copy current background styles and set initial dimensions
copyComputedBackground(inner);
setHeight();
onScroll();

// Add the overlay to the page
body.appendChild(container);

// Store original body background and clear it
bodyInlineBackgroundImageOriginal = body.style.backgroundImage;
body.style.backgroundImage = 'none';
bodyBackgroundOverridden = true;

// Ensure all body children paint above the overlay
body.classList.add('has-scroll-gradient');

// Set up resize observer for dynamic content changes (if available)
if (typeof window.ResizeObserver === 'function') {
try {
resizeObserver = new window.ResizeObserver(function () { setHeight(); });
resizeObserver.observe(document.documentElement);
} catch (_) {
resizeObserver = null;
}
}
// Add event listeners for scroll and resize
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onResize);
}
}

// Initial run
initialize();

// Cleanup on pagehide (works for both unload and BFCache)
window.addEventListener('pagehide', function () {
cleanup();
});

// Re-initialize on pageshow, especially when restored from BFCache
window.addEventListener('pageshow', function (e) {
if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] pageshow persisted=', !!(e && e.persisted));
initialize();
});
} catch {
// If we get an error, we can fail silently as we fallback to the
// original styling.
}
})();
</script>