mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Drag&Drop handler utility and animation
This commit is contained in:
@ -65,6 +65,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pulsing highlight, slightly resizing the element */
|
||||||
|
@keyframes pulse {
|
||||||
|
from {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale(1.01);
|
||||||
|
filter: brightness(1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Ellipsis animation */
|
/* Ellipsis animation */
|
||||||
@keyframes ellipsis {
|
@keyframes ellipsis {
|
||||||
0% {
|
0% {
|
||||||
|
@ -51,29 +51,33 @@ dialog {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Open state of the dialog */
|
/* Opening animation */
|
||||||
.popup[open] {
|
.popup[opening] {
|
||||||
animation: pop-in var(--animation-duration-slow) ease-in-out;
|
animation: pop-in var(--animation-duration-slow) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup[closing] {
|
.popup[opening]::backdrop {
|
||||||
animation: pop-out var(--animation-duration-slow) ease-in-out;
|
animation: fade-in var(--animation-duration-slow) ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open state of the dialog */
|
||||||
|
.popup[open] {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup[open]::backdrop {
|
.popup[open]::backdrop {
|
||||||
animation: fade-in var(--animation-duration-slow) ease-in-out;
|
|
||||||
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||||
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||||
background-color: var(--black30a);
|
background-color: var(--black30a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup[closing]::backdrop {
|
/* Closing animation */
|
||||||
animation: fade-out var(--animation-duration-slow) ease-in-out;
|
.popup[closing] {
|
||||||
|
animation: pop-out var(--animation-duration-slow) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup.dragover {
|
.popup[closing]::backdrop {
|
||||||
filter: brightness(1.1) saturate(1.1);
|
animation: fade-out var(--animation-duration-slow) ease-in-out;
|
||||||
outline: 3px dashed var(--SmartThemeBorderColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
|
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
|
||||||
|
@ -235,6 +235,7 @@ import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
|
|||||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
||||||
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
|
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
|
||||||
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
|
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
|
||||||
|
import { DragAndDropHandler } from './scripts/dragdrop.js';
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
@ -518,6 +519,7 @@ let is_delete_mode = false;
|
|||||||
let fav_ch_checked = false;
|
let fav_ch_checked = false;
|
||||||
let scrollLock = false;
|
let scrollLock = false;
|
||||||
export let abortStatusCheck = new AbortController();
|
export let abortStatusCheck = new AbortController();
|
||||||
|
let charDragDropHandler = null;
|
||||||
|
|
||||||
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
|
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
|
||||||
const durationSaveEdit = debounce_timeout.relaxed;
|
const durationSaveEdit = debounce_timeout.relaxed;
|
||||||
@ -10568,32 +10570,12 @@ jQuery(async function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const $dropzone = $(document.body);
|
charDragDropHandler = new DragAndDropHandler('body', async (files, event) => {
|
||||||
|
|
||||||
$dropzone.on('dragover', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
$dropzone.addClass('dragover');
|
|
||||||
});
|
|
||||||
|
|
||||||
$dropzone.on('dragleave', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
$dropzone.removeClass('dragover');
|
|
||||||
});
|
|
||||||
|
|
||||||
$dropzone.on('drop', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
$dropzone.removeClass('dragover');
|
|
||||||
|
|
||||||
const files = Array.from(event.originalEvent.dataTransfer.files);
|
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
await importFromURL(event.originalEvent.dataTransfer.items, files);
|
await importFromURL(event.originalEvent.dataTransfer.items, files);
|
||||||
}
|
}
|
||||||
await processDroppedFiles(files);
|
await processDroppedFiles(files);
|
||||||
});
|
}, { noAnimation: true });
|
||||||
|
|
||||||
|
|
||||||
$('#charListGridToggle').on('click', async () => {
|
$('#charListGridToggle').on('click', async () => {
|
||||||
doCharListDisplaySwitch();
|
doCharListDisplaySwitch();
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||||
import { ScraperManager } from './scrapers.js';
|
import { ScraperManager } from './scrapers.js';
|
||||||
|
import { DragAndDropHandler } from './dragdrop.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} FileAttachment
|
* @typedef {Object} FileAttachment
|
||||||
@ -962,49 +963,24 @@ async function openAttachmentManager() {
|
|||||||
template.find('.chatAttachmentsName').text(chatName);
|
template.find('.chatAttachmentsName').text(chatName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDragAndDrop() {
|
const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
|
||||||
$(document.body).on('dragover', '.popup', (event) => {
|
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
|
||||||
event.preventDefault();
|
const targets = getAvailableTargets();
|
||||||
event.stopPropagation();
|
|
||||||
$(event.target).closest('.popup').addClass('dragover');
|
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
|
||||||
|
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
|
||||||
|
selectedTarget = String($(this).val());
|
||||||
});
|
});
|
||||||
|
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
|
||||||
$(document.body).on('dragleave', '.popup', (event) => {
|
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||||
event.preventDefault();
|
console.log('File upload cancelled');
|
||||||
event.stopPropagation();
|
return;
|
||||||
$(event.target).closest('.popup').removeClass('dragover');
|
}
|
||||||
});
|
for (const file of files) {
|
||||||
|
await uploadFileAttachmentToServer(file, selectedTarget);
|
||||||
$(document.body).on('drop', '.popup', async (event) => {
|
}
|
||||||
event.preventDefault();
|
renderAttachments();
|
||||||
event.stopPropagation();
|
});
|
||||||
$(event.target).closest('.popup').removeClass('dragover');
|
|
||||||
|
|
||||||
const files = Array.from(event.originalEvent.dataTransfer.files);
|
|
||||||
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
|
|
||||||
const targets = getAvailableTargets();
|
|
||||||
|
|
||||||
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
|
|
||||||
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
|
|
||||||
selectedTarget = String($(this).val());
|
|
||||||
});
|
|
||||||
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
|
|
||||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
|
||||||
console.log('File upload cancelled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const file of files) {
|
|
||||||
await uploadFileAttachmentToServer(file, selectedTarget);
|
|
||||||
}
|
|
||||||
renderAttachments();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDragAndDrop() {
|
|
||||||
$(document.body).off('dragover', '.popup');
|
|
||||||
$(document.body).off('dragleave', '.popup');
|
|
||||||
$(document.body).off('drop', '.popup');
|
|
||||||
}
|
|
||||||
|
|
||||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||||
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
||||||
@ -1031,11 +1007,10 @@ async function openAttachmentManager() {
|
|||||||
const cleanupFn = await renderButtons();
|
const cleanupFn = await renderButtons();
|
||||||
await verifyAttachments();
|
await verifyAttachments();
|
||||||
await renderAttachments();
|
await renderAttachments();
|
||||||
addDragAndDrop();
|
|
||||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||||
|
|
||||||
cleanupFn();
|
cleanupFn();
|
||||||
removeDragAndDrop();
|
dragDropHandler.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
107
public/scripts/dragdrop.js
vendored
Normal file
107
public/scripts/dragdrop.js
vendored
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { debounce_timeout } from './constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag and drop handler
|
||||||
|
*
|
||||||
|
* Can be used on any element, enabling drag&drop styling and callback on drop.
|
||||||
|
*/
|
||||||
|
export class DragAndDropHandler {
|
||||||
|
/** @private @type {JQuery.Selector} */ selector;
|
||||||
|
/** @private @type {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} */ onDropCallback;
|
||||||
|
/** @private @type {NodeJS.Timeout} */ dragLeaveTimeout;
|
||||||
|
|
||||||
|
/** @private @type {boolean} */ noAnimation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a DragAndDropHandler
|
||||||
|
* @param {JQuery.Selector} selector - The CSS selector for the elements to enable drag and drop
|
||||||
|
* @param {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} onDropCallback - The callback function to handle the drop event
|
||||||
|
*/
|
||||||
|
constructor(selector, onDropCallback, { noAnimation = false } = {}) {
|
||||||
|
this.selector = selector;
|
||||||
|
this.onDropCallback = onDropCallback;
|
||||||
|
this.dragLeaveTimeout = null;
|
||||||
|
|
||||||
|
this.noAnimation = noAnimation;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the drag and drop functionality
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.selector === 'body') {
|
||||||
|
$(document.body).off('dragover', this.handleDragOver.bind(this));
|
||||||
|
$(document.body).off('dragleave', this.handleDragLeave.bind(this));
|
||||||
|
$(document.body).off('drop', this.handleDrop.bind(this));
|
||||||
|
} else {
|
||||||
|
$(document.body).off('dragover', this.selector, this.handleDragOver.bind(this));
|
||||||
|
$(document.body).off('dragleave', this.selector, this.handleDragLeave.bind(this));
|
||||||
|
$(document.body).off('drop', this.selector, this.handleDrop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this.selector).remove('drop_target no_animation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the drag and drop functionality
|
||||||
|
* Automatically called on construction
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (this.selector === 'body') {
|
||||||
|
$(document.body).on('dragover', this.handleDragOver.bind(this));
|
||||||
|
$(document.body).on('dragleave', this.handleDragLeave.bind(this));
|
||||||
|
$(document.body).on('drop', this.handleDrop.bind(this));
|
||||||
|
} else {
|
||||||
|
$(document.body).on('dragover', this.selector, this.handleDragOver.bind(this));
|
||||||
|
$(document.body).on('dragleave', this.selector, this.handleDragLeave.bind(this));
|
||||||
|
$(document.body).on('drop', this.selector, this.handleDrop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this.selector).addClass('drop_target');
|
||||||
|
if (this.noAnimation) $(this.selector).addClass('no_animation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {JQuery.DragOverEvent<HTMLElement, undefined, any, any>} event - The dragover event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
clearTimeout(this.dragLeaveTimeout);
|
||||||
|
$(this.selector).addClass('drop_target dragover');
|
||||||
|
if (this.noAnimation) $(this.selector).addClass('no_animation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {JQuery.DragLeaveEvent<HTMLElement, undefined, any, any>} event - The dragleave event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Debounce the removal of the class, so it doesn't "flicker" on dragging over
|
||||||
|
clearTimeout(this.dragLeaveTimeout);
|
||||||
|
this.dragLeaveTimeout = setTimeout(() => {
|
||||||
|
$(this.selector).removeClass('dragover');
|
||||||
|
}, debounce_timeout.quick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {JQuery.DropEvent<HTMLElement, undefined, any, any>} event - The drop event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
clearTimeout(this.dragLeaveTimeout);
|
||||||
|
$(this.selector).removeClass('dragover');
|
||||||
|
|
||||||
|
const files = Array.from(event.originalEvent.dataTransfer.files);
|
||||||
|
this.onDropCallback(files, event);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { dragElement } from '../../RossAscends-mods.js';
|
|||||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
|
import { DragAndDropHandler } from '../../dragdrop.js';
|
||||||
|
|
||||||
const extensionName = 'gallery';
|
const extensionName = 'gallery';
|
||||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||||
@ -56,7 +57,8 @@ async function getGalleryItems(url) {
|
|||||||
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
|
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
|
||||||
*/
|
*/
|
||||||
async function initGallery(items, url) {
|
async function initGallery(items, url) {
|
||||||
$('#dragGallery').nanogallery2({
|
const gallery = $('#dragGallery');
|
||||||
|
gallery.nanogallery2({
|
||||||
'items': items,
|
'items': items,
|
||||||
thumbnailWidth: 'auto',
|
thumbnailWidth: 'auto',
|
||||||
thumbnailHeight: thumbnailHeight,
|
thumbnailHeight: thumbnailHeight,
|
||||||
@ -80,44 +82,24 @@ async function initGallery(items, url) {
|
|||||||
|
|
||||||
|
|
||||||
eventSource.on('resizeUI', function (elmntName) {
|
eventSource.on('resizeUI', function (elmntName) {
|
||||||
jQuery('#dragGallery').nanogallery2('resize');
|
gallery.nanogallery2('resize');
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropZone = $('#dragGallery');
|
const dragDropHandler = new DragAndDropHandler('#dragGallery', async (files, event) => {
|
||||||
//remove any existing handlers
|
let file = files[0];
|
||||||
dropZone.off('dragover');
|
|
||||||
dropZone.off('dragleave');
|
|
||||||
dropZone.off('drop');
|
|
||||||
|
|
||||||
// Set dropzone height to be the same as the parent
|
|
||||||
dropZone.css('height', dropZone.parent().css('height'));
|
|
||||||
|
|
||||||
// Initialize dropzone handlers
|
|
||||||
dropZone.on('dragover', function (e) {
|
|
||||||
e.stopPropagation(); // Ensure this event doesn't propagate
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).addClass('dragging'); // Add a CSS class to change appearance during drag-over
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.on('dragleave', function (e) {
|
|
||||||
e.stopPropagation(); // Ensure this event doesn't propagate
|
|
||||||
$(this).removeClass('dragging');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.on('drop', function (e) {
|
|
||||||
e.stopPropagation(); // Ensure this event doesn't propagate
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).removeClass('dragging');
|
|
||||||
let file = e.originalEvent.dataTransfer.files[0];
|
|
||||||
uploadFile(file, url); // Added url parameter to know where to upload
|
uploadFile(file, url); // Added url parameter to know where to upload
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Set dropzone height to be the same as the parent
|
||||||
|
gallery.css('height', gallery.parent().css('height'));
|
||||||
|
|
||||||
//let images populate first
|
//let images populate first
|
||||||
await delay(100);
|
await delay(100);
|
||||||
//unset the height (which must be getting set by the gallery library at some point)
|
//unset the height (which must be getting set by the gallery library at some point)
|
||||||
$('#dragGallery').css('height', 'unset');
|
gallery.css('height', 'unset');
|
||||||
//force a resize to make images display correctly
|
//force a resize to make images display correctly
|
||||||
jQuery('#dragGallery').nanogallery2('resize');
|
gallery.nanogallery2('resize');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,11 +226,18 @@ export class Popup {
|
|||||||
async show() {
|
async show() {
|
||||||
document.body.append(this.dlg);
|
document.body.append(this.dlg);
|
||||||
|
|
||||||
|
// Run opening animation
|
||||||
|
this.dlg.setAttribute('opening', '');
|
||||||
|
|
||||||
this.dlg.showModal();
|
this.dlg.showModal();
|
||||||
|
|
||||||
// We need to fix the toastr to be present inside this dialog
|
// We need to fix the toastr to be present inside this dialog
|
||||||
fixToastrForDialogs();
|
fixToastrForDialogs();
|
||||||
|
|
||||||
|
runAfterAnimation(this.dlg, () => {
|
||||||
|
this.dlg.removeAttribute('opening');
|
||||||
|
})
|
||||||
|
|
||||||
this.promise = new Promise((resolve) => {
|
this.promise = new Promise((resolve) => {
|
||||||
this.resolver = resolve;
|
this.resolver = resolve;
|
||||||
});
|
});
|
||||||
|
@ -211,6 +211,17 @@ table.responsiveTable {
|
|||||||
animation-name: flash;
|
animation-name: flash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* General dragover styling */
|
||||||
|
.dragover {
|
||||||
|
filter: brightness(1.1) saturate(1.0);
|
||||||
|
outline: 3px dashed var(--SmartThemeBorderColor);
|
||||||
|
animation: pulse 0.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragover.no_animation {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tokenItemizingSubclass {
|
.tokenItemizingSubclass {
|
||||||
font-size: calc(var(--mainFontSize) * 0.8);
|
font-size: calc(var(--mainFontSize) * 0.8);
|
||||||
color: var(--SmartThemeEmColor);
|
color: var(--SmartThemeEmColor);
|
||||||
|
Reference in New Issue
Block a user