Layout Update (#4514) * init * mostly working messy version * css layout, mobile is fixed * revert changes * more of the above * cleanup * revert breaking fixes, cleanup dead code * dead code * delete fallback * spacing * css tweaks and removed getChatBackgroundsList * mobile sizing * revert unrelated changes * use name instead of context * debounce from constants.js * replace if/else * pass images directly * more of the above * buttons * buttons functionality * add default column counts * remove .mobile-only-menu-toggle when clicked (layering fix) * lint * universal column default of 3 * sacrifice firefox for chrome * Restore media query This reverts commit 295a5a81908e58c0bfa5e01fd90fcd62c05471b4. * Add disabled attribute styles for menu_button * use opacity 0, pointer events none instead of display:none * Format styles * Fix type errors * Add blur event handler to close mobile menu when focus is lost --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

eec837cee60d97f69dc0bdbade1720bf0cdb7a37

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

Signed
5 files changed, +234 -104Showing whitespace changes
public/css/backgrounds.css+53 -27
@@ -88,6 +88,24 @@ body.reduced-motion #bg_custom {
8888 gap: 5px;
8989}
9090
91+/* Control buttons in header */
92+.heading-container-with-controls {
93+ position: relative;
94+}
95+
96+.heading-container-with-controls .heading-text {
97+ margin: 10px 0;
98+}
99+
100+.heading-container-with-controls .heading-controls {
101+ position: absolute;
102+ right: 5px;
103+ top: 50%;
104+ transform: translateY(-50%);
105+ display: flex;
106+ gap: 5px;
107+}
108+
91109#bg-scrollable-content {
92110 flex-grow: 1;
93111 overflow-y: auto;
@@ -95,46 +113,43 @@ body.reduced-motion #bg_custom {
95113 padding: 0 5px 15px;
96114}
97115
116+#bg_menu_content,
117+#bg_custom_content {
118+ display: grid;
119+ gap: 5px;
120+ width: 100%;
121+ grid-template-columns: repeat(var(--bg-thumb-columns, 5), 1fr);
122+}
123+
98124#bg-filter {
99125 font-size: calc(var(--mainFontSize) * 0.95);
100126}
101127
102128/* Thumbnail Menu & ButtonsThumbnails */
129+.bg_example:hover .BGSampleTitle {
130+ opacity: 1;
131+}
132+
103133.bg_example .mobile-only-menu-toggle {
104134 display: none;
105135}
106136
107137.bg_example.flex-container {
108- width: 30%;
109- max-width: 200px;
110- margin: 5px;
111- aspect-ratio: 16/9;
112138 cursor: pointer;
113139 box-shadow: 0 0 7px var(--black50a);
114-
115140 position: relative;
116141 overflow: hidden;
117142 border-radius: 8px;
118143 border: 0px solid transparent;
119144 outline: 2px solid var(--SmartThemeBorderColor);
120145 outline-offset: -1px;
121-}
122146
123-.bg_example.flex-container:focus-visible {
147+ height: auto;
124148 outlineaspect-offsetratio: inherit16 / 9;
125149}
126150
127151.bg_example_imgbg_example:focus-visible {
128152 positionoutline-offset: absoluteinherit;
129- top: -2px;
130- left: -2px;
131- right: -2px;
132- bottom: -2px;
133-
134- background-image: inherit;
135-
136- background-size: cover;
137- background-position: center;
138153}
139154
140155.bg_example .jg-menu {
@@ -143,9 +158,8 @@ body.reduced-motion #bg_custom {
143158 top: 2px;
144159 right: 2px;
145160 background-color: rgba(0, 0, 0, 0.5);
146161 border-radius: 8px5px;
147162 gappadding: 3px 3px;
148- padding: 3px 5px;
149163 z-index: 3;
150164 backdrop-filter: blur(4px);
151165 border: 1px solid var(--SmartThemeBorderColor);
@@ -168,14 +182,14 @@ body.reduced-motion #bg_custom {
168182
169183.bg_example .jg-button {
170184 display: flex;
171185 width: 30px24px;
172186 height: 30px24px;
173187 align-items: center;
174188 justify-content: center;
175189 color: white;
176190 padding: 5px;
177191 font-size: 1.1em;
178192 border-radius: 6px5px;
179193 transition: background-color var(--animation-duration) ease;
180194}
181195
@@ -195,6 +209,18 @@ body.reduced-motion #bg_custom {
195209 display: flex;
196210}
197211
212+.thumbnail-clipper {
213+ position: absolute;
214+ top: -2px;
215+ left: -2px;
216+ right: -2px;
217+ bottom: -2px;
218+ overflow: hidden;
219+ border-radius: inherit;
220+ background-size: cover;
221+ background-position: center;
222+}
223+
198224.bg_example:not([custom="true"]) .jg-copy,
199225.bg_example[custom="true"] .jg-edit {
200226 display: none;
public/css/mobile-styles.css+18 -4
@@ -21,6 +21,14 @@
2121 display: none;
2222 }
2323
24+ #bg_menu_content, #bg_custom_content {
25+ grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr);
26+ }
27+
28+ .bg_list {
29+ width: unset;
30+ }
31+
2432 .bg_button {
2533 font-size: 15px;
2634 }
@@ -40,6 +48,11 @@
4048 z-index: 4;
4149 }
4250
51+ .bg_example.mobile-menu-open .mobile-only-menu-toggle {
52+ opacity: 0;
53+ pointer-events: none;
54+ }
55+
4356 .bg_example .mobile-only-menu-toggle {
4457 display: flex;
4558 align-items: center;
@@ -57,6 +70,11 @@
5770 backdrop-filter: blur(2px);
5871 }
5972
73+ .bg_example .jg-button {
74+ width: 30px;
75+ height: 30px;
76+ }
77+
6078 #bg-header-controls {
6179 flex-wrap: wrap;
6280 row-gap: 10px;
@@ -429,10 +447,6 @@
429447 .horde_multiple_hint {
430448 display: none;
431449 }
432-
433- .bg_list {
434- width: unset;
435- }
436450}
437451
438452/*landscape mode phones and ipads*/
public/index.html+11 -1
@@ -5345,9 +5345,19 @@
53455345 </div>
53465346 </div>
53475347 <div id="bg-scrollable-content">
5348- <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter">
5348+ <div class="heading-container-with-controls">
5349+ <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text">
53495350 System Backgrounds
53505351 </h3>
5352+ <div class="heading-controls">
5353+ <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller">
5354+ <i class="fa-solid fa-minus"></i>
5355+ </button>
5356+ <button id="bg_thumb_zoom_in" class="menu_button menu_button_icon" title="Make thumbnails larger" data-i18n="[title]Make thumbnails larger">
5357+ <i class="fa-solid fa-plus"></i>
5358+ </button>
5359+ </div>
5360+ </div>
53515361 <div id="bg_menu_content" class="bg_list">
53525362 </div>
53535363 <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">
public/scripts/backgrounds.js+149 -70
@@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren
33import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
44import { SlashCommand } from './slash-commands/SlashCommand.js';
55import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
66import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js';
7+import { debounce_timeout } from './constants.js';
78import { t } from './i18n.js';
89import { Popup } from './popup.js';
910
@@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY
1516const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' });
1617const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`;
1718
19+const THUMBNAIL_COLUMNS_MIN = 2;
20+const THUMBNAIL_COLUMNS_MAX = 8;
21+const THUMBNAIL_COLUMNS_DEFAULT_DESKTOP = 5;
22+const THUMBNAIL_COLUMNS_DEFAULT_MOBILE = 3;
23+
1824/**
1925 * Storage for frontend-generated background thumbnails.
2026 * This is used to store thumbnails for backgrounds that cannot be generated on the server.
@@ -45,6 +51,53 @@ export let background_settings = {
4551 animation: false,
4652};
4753
54+/**
55+ * Creates a single thumbnail DOM element. The CSS now handles all sizing.
56+ * @param {object} imageData - Data for the image (filename, isCustom).
57+ * @returns {HTMLElement} The created thumbnail element.
58+ */
59+function createThumbnailElement(imageData) {
60+ const bg = imageData.filename;
61+ const isCustom = imageData.isCustom;
62+
63+ const thumbnail = $('#background_template .bg_example').clone();
64+
65+ const clipper = document.createElement('div');
66+ clipper.className = 'thumbnail-clipper lazy-load-background';
67+ clipper.style.backgroundImage = PLACEHOLDER_IMAGE;
68+
69+ const titleElement = thumbnail.find('.BGSampleTitle');
70+ clipper.appendChild(titleElement.get(0));
71+ thumbnail.append(clipper);
72+
73+ const url = generateUrlParameter(bg, isCustom);
74+ const title = isCustom ? bg.split('/').pop() : bg;
75+ const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
76+
77+ thumbnail.attr('title', title);
78+ thumbnail.attr('bgfile', bg);
79+ thumbnail.attr('custom', String(isCustom));
80+ thumbnail.data('url', url);
81+ titleElement.text(friendlyTitle);
82+
83+ return thumbnail.get(0);
84+}
85+
86+/**
87+ * Applies the thumbnail column count to the CSS and updates button states.
88+ * @param {number} count - The number of columns to display.
89+ */
90+function applyThumbnailColumns(count) {
91+ const newCount = Math.max(THUMBNAIL_COLUMNS_MIN, Math.min(count, THUMBNAIL_COLUMNS_MAX));
92+ background_settings.thumbnailColumns = newCount;
93+ document.documentElement.style.setProperty('--bg-thumb-columns', newCount.toString());
94+
95+ $('#bg_thumb_zoom_in').prop('disabled', newCount <= THUMBNAIL_COLUMNS_MIN);
96+ $('#bg_thumb_zoom_out').prop('disabled', newCount >= THUMBNAIL_COLUMNS_MAX);
97+
98+ saveSettingsDebounced();
99+}
100+
48101export function loadBackgroundSettings(settings) {
49102 let backgroundSettings = settings.background;
50103 if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) {
@@ -56,6 +109,16 @@ export function loadBackgroundSettings(settings) {
56109 if (!Object.hasOwn(backgroundSettings, 'animation')) {
57110 backgroundSettings.animation = false;
58111 }
112+
113+ // If a value is already saved, use it. Otherwise, determine default based on screen size.
114+ let columns = backgroundSettings.thumbnailColumns;
115+ if (!columns) {
116+ const isNarrowScreen = window.matchMedia('(max-width: 480px)').matches;
117+ columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP;
118+ }
119+ background_settings.thumbnailColumns = columns;
120+ applyThumbnailColumns(background_settings.thumbnailColumns);
121+
59122 setBackground(backgroundSettings.name, backgroundSettings.url);
60123 setFittingClass(backgroundSettings.fitting);
61124 $('#background_fitting').val(backgroundSettings.fitting);
@@ -75,7 +138,7 @@ async function forceSetBackground(backgroundInfo) {
75138 list.push(bg);
76139 chat_metadata[LIST_METADATA_KEY] = list;
77140 saveMetadataDebounced();
78141 await getChatBackgroundsListrenderChatBackgrounds();
79142 highlightNewBackground(bg);
80143 highlightLockedBackground();
81144}
@@ -88,28 +151,10 @@ async function onChatChanged() {
88151 unsetCustomBackground();
89152 }
90153
91154 await getChatBackgroundsListrenderChatBackgrounds();
92155 highlightLockedBackground();
93156}
94157
95-async function getChatBackgroundsList() {
96- const list = chat_metadata[LIST_METADATA_KEY];
97- const listEmpty = !Array.isArray(list) || list.length === 0;
98-
99- $('#bg_custom_content').empty();
100- $('#bg_chat_hint').toggle(listEmpty);
101-
102- if (listEmpty) {
103- return;
104- }
105-
106- for (const bg of list) {
107- const template = await getBackgroundFromTemplate(bg, true);
108- $('#bg_custom_content').append(template);
109- }
110- activateLazyLoader();
111-}
112-
113158function getBackgroundPath(fileUrl) {
114159 return `backgrounds/${encodeURIComponent(fileUrl)}`;
115160}
@@ -257,7 +302,7 @@ async function onCopyToSystemBackgroundClick(e) {
257302 const index = list.indexOf(bgNames.oldBg);
258303 list.splice(index, 1);
259304 saveMetadataDebounced();
260305 await getChatBackgroundsListrenderChatBackgrounds();
261306}
262307
263308/**
@@ -382,17 +427,21 @@ async function onDeleteBackgroundClick(e) {
382427 list.splice(index, 1);
383428 }
384429
385- const siblingSelector = '.bg_example:not(#form_bg_download)';
430+ if (bg === background_settings.name) {
431+ const siblingSelector = '.bg_example';
386432 const nextBg = bgToDelete.next(siblingSelector);
387433 const prevBg = bgToDelete.prev(siblingSelector);
388- const anyBg = $(siblingSelector);
389434
390435 if (nextBg.length > 0) {
391436 nextBg.trigger('click');
392437 } else if (prevBg.length > 0) {
393438 prevBg.trigger('click');
394439 } else {
395- $(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
440+ const anyOtherBg = $('.bg_example').not(bgToDelete).first();
441+ if (anyOtherBg.length > 0) {
442+ anyOtherBg.trigger('click');
443+ }
444+ }
396445 }
397446
398447 bgToDelete.remove();
@@ -404,7 +453,7 @@ async function onDeleteBackgroundClick(e) {
404453 }
405454
406455 if (isCustom) {
407456 await getChatBackgroundsListrenderChatBackgrounds();
408457 saveMetadataDebounced();
409458 }
410459 }
@@ -445,6 +494,47 @@ async function autoBackgroundCommand() {
445494 return '';
446495}
447496
497+/**
498+ * Renders the system backgrounds gallery.
499+ * @param {string[]} [backgrounds] - Optional filtered list of backgrounds.
500+ */
501+function renderSystemBackgrounds(backgrounds) {
502+ const sourceList = backgrounds || [];
503+ const container = $('#bg_menu_content');
504+ container.empty();
505+
506+ if (sourceList.length === 0) return;
507+
508+ sourceList.forEach(bg => {
509+ const imageData = { filename: bg, isCustom: false };
510+ const thumbnail = createThumbnailElement(imageData);
511+ container.append(thumbnail);
512+ });
513+
514+ activateLazyLoader();
515+}
516+
517+/**
518+ * Renders the chat-specific (custom) backgrounds gallery.
519+ * @param {string[]} [backgrounds] - Optional filtered list of backgrounds.
520+ */
521+function renderChatBackgrounds(backgrounds) {
522+ const sourceList = backgrounds ?? (chat_metadata[LIST_METADATA_KEY] || []);
523+ const container = $('#bg_custom_content');
524+ container.empty();
525+ $('#bg_chat_hint').toggle(!sourceList.length);
526+
527+ if (sourceList.length === 0) return;
528+
529+ sourceList.forEach(bg => {
530+ const imageData = { filename: bg, isCustom: true };
531+ const thumbnail = createThumbnailElement(imageData);
532+ container.append(thumbnail);
533+ });
534+
535+ activateLazyLoader();
536+}
537+
448538export async function getBackgrounds() {
449539 const response = await fetch('/api/backgrounds/all', {
450540 method: 'POST',
@@ -454,12 +544,7 @@ export async function getBackgrounds() {
454544 if (response.ok) {
455545 const { images, config } = await response.json();
456546 Object.assign(THUMBNAIL_CONFIG, config);
457- $('#bg_menu_content').children('div').remove();
547+ renderSystemBackgrounds(images);
458- for (const bg of images) {
459- const template = await getBackgroundFromTemplate(bg, false);
460- $('#bg_menu_content').append(template);
461- }
462- activateLazyLoader();
463548 }
464549}
465550
@@ -481,14 +566,19 @@ function activateLazyLoader() {
481566 lazyLoadObserver = new IntersectionObserver((entries, observer) => {
482567 entries.forEach(entry => {
483568 if (entry.target instanceof HTMLElement && entry.isIntersecting) {
484569 const targetclipper = entry.target;
485570 const bgparentThumbnail = targetclipper.getAttributeclosest('bgfile.bg_example');
486- const isCustom = target.getAttribute('custom') === 'true';
571+
572+ if (parentThumbnail) {
573+ const bg = parentThumbnail.getAttribute('bgfile');
574+ const isCustom = parentThumbnail.getAttribute('custom') === 'true';
487575 resolveImageUrl(bg, isCustom)
488576 .then(url => { targetclipper.style.backgroundImage = url; })
489577 .catch(() => { targetclipper.style.backgroundImage = PLACEHOLDER_IMAGE; });
490- target.classList.remove('lazy-load-background');
578+ }
491- observer.unobserve(target);
579+
580+ clipper.classList.remove('lazy-load-background');
581+ observer.unobserve(clipper);
492582 }
493583 });
494584 }, options);
@@ -529,28 +619,6 @@ async function resolveImageUrl(bg, isCustom) {
529619 return `url("${thumbnailUrl}")`;
530620}
531621
532-/**
533- * Instantiates a background template
534- * @param {string} bg Path to background
535- * @param {boolean} isCustom Whether the background is custom
536- * @returns {Promise<JQuery<HTMLElement>>} Background template
537- */
538-async function getBackgroundFromTemplate(bg, isCustom) {
539- const template = $('#background_template .bg_example').clone();
540- const url = generateUrlParameter(bg, isCustom);
541- const title = isCustom ? bg.split('/').pop() : bg;
542- const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
543-
544- template.attr('title', title);
545- template.attr('bgfile', bg);
546- template.attr('custom', String(isCustom));
547- template.data('url', url);
548- template.addClass('lazy-load-background');
549- template.css('background-image', PLACEHOLDER_IMAGE);
550- template.find('.BGSampleTitle').text(friendlyTitle);
551- return template;
552-}
553-
554622async function setBackground(bg, url) {
555623 $('#bg1').css('background-image', url);
556624 background_settings.name = bg;
@@ -688,17 +756,17 @@ function setFittingClass(fitting) {
688756}
689757
690758function onBackgroundFilterInput() {
691759 const filterValue = String($(this'#bg-filter').val()).toLowerCase();
692760 $('#bg_menu_content > div.bg_example, #bg_custom_content > .bg_example').each(function () {
693761 const $bgContentbg = $(this);
694- if ($bgContent.attr('title').toLowerCase().includes(filterValue)) {
762+ const title = $bg.attr('title') || '';
695- $bgContent.show();
763+ const hasMatch = title.toLowerCase().includes(filterValue);
696- } else {
764+ $bg.toggle(hasMatch);
697- $bgContent.hide();
698- }
699765 });
700766}
701767
768+const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard);
769+
702770export function initBackgrounds() {
703771 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
704772 eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
@@ -715,6 +783,11 @@ export function initBackgrounds() {
715783 $context.addClass('mobile-menu-open');
716784 }
717785 })
786+ .off('blur', '.bg_example.mobile-menu-open').on('blur', '.bg_example.mobile-menu-open', function () {
787+ if (!$(this).is(':focus-within')) {
788+ $(this).removeClass('mobile-menu-open');
789+ }
790+ })
718791 .off('click', '.jg-button').on('click', '.jg-button', function (e) {
719792 e.stopPropagation();
720793 const action = $(this).data('action');
@@ -738,9 +811,15 @@ export function initBackgrounds() {
738811 }
739812 });
740813
814+ $('#bg_thumb_zoom_in').on('click', () => {
815+ applyThumbnailColumns(background_settings.thumbnailColumns - 1);
816+ });
817+ $('#bg_thumb_zoom_out').on('click', () => {
818+ applyThumbnailColumns(background_settings.thumbnailColumns + 1);
819+ });
741820 $('#auto_background').on('click', autoBackgroundCommand);
742821 $('#add_bg_button').on('change', onBackgroundUploadSelected);
743822 $('#bg-filter').on('input', onBackgroundFilterInput() => debouncedOnBackgroundFilterInput());
744823 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
745824 name: 'lockbg',
746825 callback: () => onLockBackgroundClick(new CustomEvent('click')),
public/style.css+3 -2
@@ -2940,6 +2940,7 @@ select option:not(:checked) {
29402940 text-align: center;
29412941}
29422942
2943+.menu_button[disabled],
29432944.menu_button.disabled {
29442945 filter: brightness(75%) grayscale(1);
29452946 opacity: 0.5;
@@ -3675,8 +3676,8 @@ grammarly-extension {
36753676 min-width: calc(1.25em + 12px);
36763677}
36773678
36783679.menu_button:not(.disabled):not([disabled]):hover,
36793680.menu_button:not(.disabled):not([disabled]).active {
36803681 background-color: var(--white30a);
36813682}
36823683