Scroll to top button (#4569) * init * implement review suggestions * remove unnecessary css variables * respect reduced motion * simplify setupScrollToTop call * power_user flag fix * simplify, more of above * remove remove button when drawer is closed logic * throttle scroll checker * Fix review comments * Make scrollToTop a shared function * Refactor logic * Replace on hover color * Prefer theme colors for button * Increase hover brightness for scroll-to-top button * Clean-up diff * Fix type annotation --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

7f9cbb04c6d1b2045f65d1fc77d94657fe3d2419

L <123923688+Vibecoder9000@users.noreply.github.com>

Signed
4 files changed, +100 -1Showing whitespace changes
public/css/backgrounds.css+39 -0
@@ -88,6 +88,7 @@
88 overflow-y: auto;88 overflow-y: auto;
89 overflow-x: hidden;89 overflow-x: hidden;
90 padding: 0 5px 15px;90 padding: 0 5px 15px;
91 position: relative;
91}92}
9293
93#bg_menu_content,94#bg_menu_content,
@@ -219,6 +220,44 @@
219 background-color: rgba(255, 255, 255, 0.2);220 background-color: rgba(255, 255, 255, 0.2);
220}221}
221222
223/* Scroll-to-Top Button */
224#bg-scroll-top {
225 position: absolute;
226 bottom: 20px;
227 right: 20px;
228 width: 42px;
229 height: 42px;
230 background: var(--SmartThemeBlurTintColor);
231 color: var(--SmartThemeBodyColor);
232 border: 1px solid var(--SmartThemeBorderColor);
233 border-radius: 50%;
234 cursor: pointer;
235 z-index: 10;
236 font-size: 18px;
237 display: inline-flex;
238 align-items: center;
239 justify-content: center;
240 opacity: 0;
241 transition: opacity var(--animation-duration) ease;
242 pointer-events: none;
243}
244
245#bg-scroll-top.visible {
246 opacity: 1;
247 pointer-events: auto;
248}
249
250#bg-scroll-top:hover {
251 filter: brightness(150%);
252 outline: 1px solid var(--interactable-outline-color);
253}
254
255#bg-scroll-top .fa-solid {
256 margin: 0;
257 padding: 0;
258 line-height: 1;
259}
260
222.thumbnail-clipper {261.thumbnail-clipper {
223 position: absolute;262 position: absolute;
224 top: -2px;263 top: -2px;
public/index.html+3 -0
@@ -5351,6 +5351,9 @@
5351 <form id="form_bg_upload" style="display: none;">5351 <form id="form_bg_upload" style="display: none;">
5352 <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*">5352 <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*">
5353 </form>5353 </form>
5354 <button id="bg-scroll-top" type="button" class="menu_button menu_button_icon" title="Scroll backgrounds to top" data-i18n="[title]Scroll backgrounds to top">
5355 <i class="fa-solid fa-chevron-up" aria-hidden="true"></i>
5356 </button>
5354 </div>5357 </div>
5355 </div>5358 </div>
5356 <div id="extensions-settings-button" class="drawer">5359 <div id="extensions-settings-button" class="drawer">
public/scripts/backgrounds.js+7 -1
@@ -3,7 +3,7 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren
3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop } from './utils.js';
7import { debounce_timeout } from './constants.js';7import { debounce_timeout } from './constants.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
@@ -838,4 +838,10 @@ export function initBackgrounds() {
838 await getBackgrounds();838 await getBackgrounds();
839 await onChatChanged();839 await onChatChanged();
840 });840 });
841
842 setupScrollToTop({
843 scrollContainerId: 'bg-scrollable-content',
844 buttonId: 'bg-scroll-top',
845 drawerId: 'Backgrounds',
846 });
841}847}
public/scripts/utils.js+51 -0
@@ -3,6 +3,7 @@ import {
3 DOMPurify,3 DOMPurify,
4 Readability,4 Readability,
5 isProbablyReaderable,5 isProbablyReaderable,
6 lodash,
6} from '../lib.js';7} from '../lib.js';
78
8import { getContext } from './extensions.js';9import { getContext } from './extensions.js';
@@ -2562,3 +2563,53 @@ export function textValueMatcher(params, data) {
2562export function versionCompare(srcVersion, minVersion) {2563export function versionCompare(srcVersion, minVersion) {
2563 return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;2564 return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;
2564}2565}
2566
2567/**
2568 * Sets up the scroll-to-top button functionality.
2569 * @param {object} params Parameters object
2570 * @param {string} params.scrollContainerId Scrollable container element ID
2571 * @param {string} params.buttonId Button element ID
2572 * @param {string} params.drawerId Drawer element ID
2573 * @param {number} [params.visibilityThreshold] Scroll position (px) to show the button (default: 300)
2574 * @returns {() => void} Cleanup function to remove event listeners
2575 */
2576export function setupScrollToTop({ scrollContainerId, buttonId, drawerId, visibilityThreshold = 300 }) {
2577 const scrollContainer = document.getElementById(scrollContainerId);
2578 const btn = document.getElementById(buttonId);
2579 const drawer = document.getElementById(drawerId);
2580
2581 if (!btn || !drawer) {
2582 // Not fatal; the drawer or button may not exist in some builds. Use debug level.
2583 console.debug('Scroll-to-top: button or drawer not found during setup.');
2584 return () => { /* noop cleanup */ };
2585 }
2586
2587 if (!scrollContainer) {
2588 console.debug('Scroll-to-top: scroll container not found during setup.');
2589 return () => { /* noop cleanup */ };
2590 }
2591
2592 const updateButtonVisibility = () => btn.classList.toggle('visible', scrollContainer.scrollTop > visibilityThreshold);
2593 const updateButtonVisibilityThrottled = lodash.throttle(updateButtonVisibility, debounce_timeout.standard, { leading: true, trailing: true });
2594 const onScroll = () => updateButtonVisibilityThrottled();
2595 scrollContainer.addEventListener('scroll', onScroll, { passive: true });
2596
2597 // Scroll to top on click (button semantics provide keyboard activation natively)
2598 const onActivate = (/** @type {MouseEvent} */ e) => {
2599 e.preventDefault();
2600 e.stopPropagation();
2601
2602 const userPrefersReduced = power_user.reduced_motion;
2603 scrollContainer.scrollTo({ top: 0, behavior: userPrefersReduced ? 'auto' : 'smooth' });
2604 };
2605 btn.addEventListener('click', onActivate);
2606
2607 // Initial state check
2608 updateButtonVisibility();
2609
2610 // Return cleanup function for caller to hold and invoke when appropriate
2611 return () => {
2612 scrollContainer.removeEventListener('scroll', onScroll);
2613 btn.removeEventListener('click', onActivate);
2614 };
2615}