diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index ec6b74f5..a51b7e58 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -40,7 +40,8 @@ coverImage: null, isEditable: true, hideThemeName: false, - forceDark: undefined + forceDark: undefined, + adaptiveCssVariables: [], }) let closedAccordions = $state([]) let themeLoaded = $state(false); @@ -80,7 +81,10 @@ })) } - theme = loadedTheme + theme = { + ...loadedTheme, + adaptiveCssVariables: loadedTheme.adaptiveCssVariables ?? [], + } themeLoaded = true } else { themeLoaded = true @@ -317,6 +321,27 @@ +
+

Adaptive CSS variables

+

+ One per line, each must start with --. These receive the same colour as the adaptive accent when "Adaptive theme colour" is enabled in general settings. Use them in Custom CSS, e.g. border-color: var(--my-accent); +

+ +
+ + + {#each [ { type: 'switch', diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 488e376b..cd499c3a 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -3,6 +3,11 @@ import browser from "webextension-polyfill"; import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import debounce from "@/seqta/utils/debounce"; +import { updateAllColors } from "@/seqta/ui/colors/Manager"; +import { + clearCustomThemeAdaptiveCssVariables, + setCustomThemeAdaptiveCssVariables, +} from "@/seqta/ui/colors/customThemeAdaptiveBindings"; type ThemeContent = { id: string; @@ -14,6 +19,7 @@ type ThemeContent = { CustomCSS?: string; hideThemeName?: boolean; forceDark?: boolean; + adaptiveCssVariables?: string[]; images: { id: string; variableName: string; data: string }[]; // data: base64 }; @@ -240,6 +246,7 @@ export class ThemeManager { this.currentTheme = theme; settingsState.selectedTheme = themeId; } + void updateAllColors(); } catch (error) { console.error("[ThemeManager] Error setting theme:", error); } @@ -289,6 +296,8 @@ export class ThemeManager { ); settingsState.selectedColor = theme.defaultColour; } + + setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []); } catch (error) { console.error("[ThemeManager] Error applying theme:", error); } @@ -373,6 +382,7 @@ export class ThemeManager { if (clearSelectedTheme) { settingsState.selectedTheme = ""; } + clearCustomThemeAdaptiveCssVariables(); } catch (error) { console.error("[ThemeManager] Error removing theme:", error); } @@ -585,6 +595,7 @@ export class ThemeManager { isEditable: false, hideThemeName: themeData.hideThemeName ?? false, forceDark: themeData.forceDark, + adaptiveCssVariables: themeData.adaptiveCssVariables, }; await this.saveTheme(theme); @@ -704,6 +715,9 @@ export class ThemeManager { if (defaultColour) { settingsState.selectedColor = defaultColour; } + + setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []); + void updateAllColors(); } catch (error) { console.error("[ThemeManager] Error previewing theme:", error); } @@ -778,6 +792,9 @@ export class ThemeManager { if (!theme.webURL && theme.defaultColour) { settingsState.selectedColor = theme.defaultColour; } + + setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []); + void updateAllColors(); } catch (error) { console.error("[ThemeManager] Error updating theme preview:", error); } @@ -815,6 +832,8 @@ export class ThemeManager { this.previewStyleElement = null; } + clearCustomThemeAdaptiveCssVariables(); + // Restore original settings const storedColor = localStorage.getItem("originalPreviewColor"); @@ -844,6 +863,8 @@ export class ThemeManager { settingsState.DarkMode = this.originalPreviewTheme; this.originalPreviewTheme = null; } + + void updateAllColors(); } catch (error) { console.error("[ThemeManager] Error clearing preview:", error); } diff --git a/src/seqta/ui/colors/Manager.ts b/src/seqta/ui/colors/Manager.ts index f035fe12..d72c8844 100644 --- a/src/seqta/ui/colors/Manager.ts +++ b/src/seqta/ui/colors/Manager.ts @@ -5,6 +5,7 @@ import { lightenAndPaleColor } from "./lightenAndPaleColor"; import ColorLuminance from "./ColorLuminance"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour"; +import { getCustomThemeAdaptiveCssVariables } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; @@ -127,6 +128,12 @@ function applyColorsWith(selectedColor: string) { // Apply all the properties applyProperties({ ...commonProps, ...modeProps, ...dynamicProps }); + if (settingsState.selectedTheme) { + for (const name of getCustomThemeAdaptiveCssVariables()) { + setCSSVar(name, selectedColor); + } + } + let alliframes = document.getElementsByTagName("iframe"); for (let i = 0; i < alliframes.length; i++) { diff --git a/src/seqta/ui/colors/customThemeAdaptiveBindings.ts b/src/seqta/ui/colors/customThemeAdaptiveBindings.ts new file mode 100644 index 00000000..dba35e3f --- /dev/null +++ b/src/seqta/ui/colors/customThemeAdaptiveBindings.ts @@ -0,0 +1,40 @@ +/** Tracks which author-declared CSS variables mirror the effective accent; not persisted in settings storage. */ + +const VALID_CUSTOM_PROP = /^--[a-zA-Z0-9_-]{1,120}$/; + +let boundNames: string[] = []; + +export function normalizeAdaptiveCssVariableNames( + names: string[] | undefined, +): string[] { + if (!names?.length) return []; + const out: string[] = []; + const seen = new Set(); + for (const raw of names) { + const s = raw.trim(); + if (!VALID_CUSTOM_PROP.test(s) || seen.has(s)) continue; + seen.add(s); + out.push(s); + } + return out; +} + +export function setCustomThemeAdaptiveCssVariables( + names: string[] | undefined, +): void { + for (const n of boundNames) { + document.documentElement.style.removeProperty(n); + } + boundNames = normalizeAdaptiveCssVariableNames(names); +} + +export function getCustomThemeAdaptiveCssVariables(): string[] { + return boundNames; +} + +export function clearCustomThemeAdaptiveCssVariables(): void { + for (const n of boundNames) { + document.documentElement.style.removeProperty(n); + } + boundNames = []; +} diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 7e178379..2d9ccf06 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -20,6 +20,7 @@ export class StorageChangeHandler { settingsState.register("adaptiveThemeColourTransition", () => void updateAllColors(), ); + settingsState.register("selectedTheme", () => void updateAllColors()); settingsState.register("DarkMode", this.handleDarkModeChange.bind(this)); settingsState.register("onoff", this.handleOnOffChange.bind(this)); settingsState.register("shortcuts", this.handleShortcutsChange.bind(this)); diff --git a/src/types/CustomThemes.ts b/src/types/CustomThemes.ts index a8620db1..92498428 100644 --- a/src/types/CustomThemes.ts +++ b/src/types/CustomThemes.ts @@ -13,6 +13,8 @@ export type CustomTheme = { webURL?: string; selectedColor?: string; forceDark?: boolean; + /** CSS custom property names (e.g. `--my-accent`) that receive the same value as `--better-main` when adaptive colours apply. */ + adaptiveCssVariables?: string[]; }; export type LoadedCustomTheme = CustomTheme & {