Merge pull request #3284 from SillyTavern/css-var-slash-command Add `/css-var` slash command

1807af355b3e4b4aedc1bd37edad95fa845c3221

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +108 -6Showing whitespace changes
public/scripts/extensions.js+7 -5
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
4import { showLoader } from './loader.js';4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';6import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, isSubsetOf, setValueByPath } from './utils.js';7import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js';
8import { getContext } from './st-context.js';8import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { t } from './i18n.js';10import { t } from './i18n.js';
@@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) {
509509
510 return new Promise((resolve, reject) => {510 return new Promise((resolve, reject) => {
511 const url = `/scripts/extensions/${name}/${manifest.css}`;511 const url = `/scripts/extensions/${name}/${manifest.css}`;
512 const id = sanitizeSelector(`${name}-css`);
512513
513 if ($(`link[id="${name}"]`).length === 0) {514 if ($(`link[id="${id}"]`).length === 0) {
514 const link = document.createElement('link');515 const link = document.createElement('link');
515 link.id = name;516 link.id = id;
516 link.rel = 'stylesheet';517 link.rel = 'stylesheet';
517 link.type = 'text/css';518 link.type = 'text/css';
518 link.href = url;519 link.href = url;
@@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) {
540541
541 return new Promise((resolve, reject) => {542 return new Promise((resolve, reject) => {
542 const url = `/scripts/extensions/${name}/${manifest.js}`;543 const url = `/scripts/extensions/${name}/${manifest.js}`;
544 const id = sanitizeSelector(`${name}-js`);
543 let ready = false;545 let ready = false;
544546
545 if ($(`script[id="${name}"]`).length === 0) {547 if ($(`script[id="${id}"]`).length === 0) {
546 const script = document.createElement('script');548 const script = document.createElement('script');
547 script.id = name;549 script.id = id;
548 script.type = 'module';550 script.type = 'module';
549 script.src = url;551 script.src = url;
550 script.async = true;552 script.async = true;
public/scripts/extensions/gallery/index.js+1 -1
@@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') {
277 const newElement = $(template);277 const newElement = $(template);
278 newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');278 newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
279 newElement.attr('forChar', id);279 newElement.attr('forChar', id);
280 newElement.attr('id', `${id}`);280 newElement.attr('id', id);
281 newElement.find('.drag-grabber').attr('id', `${id}header`);281 newElement.find('.drag-grabber').attr('id', `${id}header`);
282 newElement.find('.dragTitle').text('Image Gallery');282 newElement.find('.dragTitle').text('Image Gallery');
283 //add a div for the gallery283 //add a div for the gallery
public/scripts/power-user.js+89 -0
@@ -3963,6 +3963,95 @@ $(document).ready(() => {
3963 `,3963 `,
3964 }));3964 }));
3965 SlashCommandParser.addCommandObject(SlashCommand.fromProps({3965 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3966 name: 'css-var',
3967 /** @param {{to: string, varname: string }} args @param {string} value @returns {string} */
3968 callback: (args, value) => {
3969 // Map enum to target selector
3970 const targetSelector = {
3971 chat: '#chat',
3972 background: '#bg1',
3973 gallery: '#gallery',
3974 zoomedAvatar: 'div.zoomed_avatar',
3975 }[args.to || 'chat'];
3976
3977 if (!targetSelector) {
3978 toastr.error(`Invalid target: ${args.to}`);
3979 return;
3980 }
3981
3982 if (!args.varname) {
3983 toastr.error('CSS variable name is required');
3984 return;
3985 }
3986 if (!args.varname.startsWith('--')) {
3987 toastr.error('CSS variable names must start with "--"');
3988 return;
3989 }
3990
3991 const elements = document.querySelectorAll(targetSelector);
3992 if (elements.length === 0) {
3993 toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`);
3994 return;
3995 }
3996
3997 elements.forEach(element => {
3998 element.style.setProperty(args.varname, value);
3999 });
4000
4001 console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`);
4002 },
4003 namedArgumentList: [
4004 SlashCommandNamedArgument.fromProps({
4005 name: 'varname',
4006 description: 'CSS variable name (starting with double dashes)',
4007 typeList: [ARGUMENT_TYPE.STRING],
4008 isRequired: true,
4009 }),
4010 SlashCommandNamedArgument.fromProps({
4011 name: 'to',
4012 description: 'The target element to which the CSS variable will be applied',
4013 typeList: [ARGUMENT_TYPE.STRING],
4014 enumList: [
4015 new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message),
4016 new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image),
4017 new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character),
4018 new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image),
4019 ],
4020 defaultValue: 'chat',
4021 }),
4022 ],
4023 unnamedArgumentList: [
4024 SlashCommandArgument.fromProps({
4025 description: 'CSS variable value',
4026 typeList: [ARGUMENT_TYPE.STRING],
4027 isRequired: true,
4028 }),
4029 ],
4030 helpString: `
4031 <div>
4032 Sets a CSS variable to a specified value on a target element.
4033 <br />
4034 Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar").
4035 Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that.
4036 <br /><br />
4037 <b>This value will be gone after a page reload!</b>
4038 </div>
4039 <div>
4040 <strong>Example:</strong>
4041 <ul>
4042 <li>
4043 <pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
4044 Sets the text color of the chat to red
4045 </li>
4046 <li>
4047 <pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
4048 Remove the blur from the zoomed avatar
4049 </li>
4050 </ul>
4051 </div>
4052 `,
4053 }));
4054 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
3966 name: 'movingui',4055 name: 'movingui',
3967 callback: setmovingUIPreset,4056 callback: setmovingUIPreset,
3968 unnamedArgumentList: [4057 unnamedArgumentList: [
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -37,6 +37,7 @@ export const enumIcons = {
37 voice: '🎤',37 voice: '🎤',
38 server: '🖥️',38 server: '🖥️',
39 popup: '🗔',39 popup: '🗔',
40 image: '🖼️',
4041
41 true: '✔️',42 true: '✔️',
42 false: '❌',43 false: '❌',
public/scripts/utils.js+10 -0
@@ -67,6 +67,16 @@ export function escapeHtml(str) {
67 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');67 return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
68}68}
6969
70/**
71 * Make string safe for use as a CSS selector.
72 * @param {string} str String to sanitize
73 * @param {string} replacement Replacement for invalid characters
74 * @returns {string} Sanitized string
75 */
76export function sanitizeSelector(str, replacement = '_') {
77 return String(str).replace(/[^a-z0-9_-]/ig, replacement);
78}
79
70export function isValidUrl(value) {80export function isValidUrl(value) {
71 try {81 try {
72 new URL(value);82 new URL(value);