From 7aeb8fbb650c1a4fad417a690b8261574d118d33 Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Sun, 22 Mar 2026 02:17:57 +0800 Subject: [PATCH] feat(website): add back-to-top button Adds a fixed-position button that fades in after scrolling 600px and smoothly scrolls back to the top on click. Hidden by default via the HTML hidden attribute so noscript users never see it. Co-Authored-By: Claude --- website/static/main.js | 19 ++++++++++++++++ website/static/style.css | 45 +++++++++++++++++++++++++++++++++++++ website/templates/base.html | 2 ++ 3 files changed, 66 insertions(+) diff --git a/website/static/main.js b/website/static/main.js index 7168986..9ad9534 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -279,6 +279,25 @@ if (searchInput) { }); } +// Back to top +var backToTop = document.querySelector('.back-to-top'); +if (backToTop) { + backToTop.hidden = false; + var scrollTicking = false; + window.addEventListener('scroll', function () { + if (!scrollTicking) { + requestAnimationFrame(function () { + backToTop.classList.toggle('visible', window.scrollY > 600); + scrollTicking = false; + }); + scrollTicking = true; + } + }); + backToTop.addEventListener('click', function () { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); +} + // Restore state from URL (function () { var params = new URLSearchParams(location.search); diff --git a/website/static/style.css b/website/static/style.css index ce2f607..304c865 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -527,6 +527,51 @@ th[data-sort].sort-asc::after { font-weight: 600; } +/* === Back to Top === */ +.back-to-top { + position: fixed; + bottom: 2rem; + right: max(1.5rem, calc(50vw - 700px + 0.5rem)); + background: var(--bg); + border: 1px solid var(--border-strong); + border-radius: 4px; + padding: 0.4rem 0.75rem; + font-family: var(--font-body); + font-size: var(--text-xs); + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + opacity: 0; + transform: translateY(0.5rem); + transition: opacity 0.25s ease, transform 0.25s ease, border-color 0.15s, color 0.15s; + z-index: 50; +} + +.back-to-top[hidden] { display: none; } + +.back-to-top.visible { + opacity: 1; + transform: translateY(0); +} + +.back-to-top:hover { + border-color: var(--accent); + color: var(--accent); +} + +.back-to-top:active { + transform: scale(0.95); +} + +.back-to-top:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +@media (max-width: 640px) { + .back-to-top { right: 1.25rem; bottom: 1.25rem; } +} + /* === Noscript === */ .noscript-msg { text-align: center; diff --git a/website/templates/base.html b/website/templates/base.html index ea7c7e5..2afe5eb 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -37,6 +37,8 @@
{% block content %}{% endblock %}
+ +