mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: Themes can adapt to colour
This commit is contained in:
@@ -40,7 +40,8 @@
|
|||||||
coverImage: null,
|
coverImage: null,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
hideThemeName: false,
|
hideThemeName: false,
|
||||||
forceDark: undefined
|
forceDark: undefined,
|
||||||
|
adaptiveCssVariables: [],
|
||||||
})
|
})
|
||||||
let closedAccordions = $state<string[]>([])
|
let closedAccordions = $state<string[]>([])
|
||||||
let themeLoaded = $state(false);
|
let themeLoaded = $state(false);
|
||||||
@@ -80,7 +81,10 @@
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
theme = loadedTheme
|
theme = {
|
||||||
|
...loadedTheme,
|
||||||
|
adaptiveCssVariables: loadedTheme.adaptiveCssVariables ?? [],
|
||||||
|
}
|
||||||
themeLoaded = true
|
themeLoaded = true
|
||||||
} else {
|
} else {
|
||||||
themeLoaded = true
|
themeLoaded = true
|
||||||
@@ -317,6 +321,27 @@
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<div class="py-3">
|
||||||
|
<h2 class="text-sm font-bold">Adaptive CSS variables</h2>
|
||||||
|
<p class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||||
|
One per line, each must start with <code class="text-xs">--</code>. 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. <code class="text-xs">border-color: var(--my-accent);</code>
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
placeholder="--my-accent --class-banner"
|
||||||
|
value={theme.adaptiveCssVariables?.join('\n') ?? ''}
|
||||||
|
oninput={(e) => {
|
||||||
|
const lines = e.currentTarget.value
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
theme = { ...theme, adaptiveCssVariables: lines };
|
||||||
|
}}
|
||||||
|
class="p-2 mt-2 w-full min-h-[5rem] font-mono text-sm rounded-lg border-0 transition dark:placeholder-zinc-400 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
{#each [
|
{#each [
|
||||||
{
|
{
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import browser from "webextension-polyfill";
|
|||||||
import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
|
import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import debounce from "@/seqta/utils/debounce";
|
import debounce from "@/seqta/utils/debounce";
|
||||||
|
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
||||||
|
import {
|
||||||
|
clearCustomThemeAdaptiveCssVariables,
|
||||||
|
setCustomThemeAdaptiveCssVariables,
|
||||||
|
} from "@/seqta/ui/colors/customThemeAdaptiveBindings";
|
||||||
|
|
||||||
type ThemeContent = {
|
type ThemeContent = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,6 +19,7 @@ type ThemeContent = {
|
|||||||
CustomCSS?: string;
|
CustomCSS?: string;
|
||||||
hideThemeName?: boolean;
|
hideThemeName?: boolean;
|
||||||
forceDark?: boolean;
|
forceDark?: boolean;
|
||||||
|
adaptiveCssVariables?: string[];
|
||||||
images: { id: string; variableName: string; data: string }[]; // data: base64
|
images: { id: string; variableName: string; data: string }[]; // data: base64
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +246,7 @@ export class ThemeManager {
|
|||||||
this.currentTheme = theme;
|
this.currentTheme = theme;
|
||||||
settingsState.selectedTheme = themeId;
|
settingsState.selectedTheme = themeId;
|
||||||
}
|
}
|
||||||
|
void updateAllColors();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error setting theme:", error);
|
console.error("[ThemeManager] Error setting theme:", error);
|
||||||
}
|
}
|
||||||
@@ -289,6 +296,8 @@ export class ThemeManager {
|
|||||||
);
|
);
|
||||||
settingsState.selectedColor = theme.defaultColour;
|
settingsState.selectedColor = theme.defaultColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error applying theme:", error);
|
console.error("[ThemeManager] Error applying theme:", error);
|
||||||
}
|
}
|
||||||
@@ -373,6 +382,7 @@ export class ThemeManager {
|
|||||||
if (clearSelectedTheme) {
|
if (clearSelectedTheme) {
|
||||||
settingsState.selectedTheme = "";
|
settingsState.selectedTheme = "";
|
||||||
}
|
}
|
||||||
|
clearCustomThemeAdaptiveCssVariables();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error removing theme:", error);
|
console.error("[ThemeManager] Error removing theme:", error);
|
||||||
}
|
}
|
||||||
@@ -585,6 +595,7 @@ export class ThemeManager {
|
|||||||
isEditable: false,
|
isEditable: false,
|
||||||
hideThemeName: themeData.hideThemeName ?? false,
|
hideThemeName: themeData.hideThemeName ?? false,
|
||||||
forceDark: themeData.forceDark,
|
forceDark: themeData.forceDark,
|
||||||
|
adaptiveCssVariables: themeData.adaptiveCssVariables,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.saveTheme(theme);
|
await this.saveTheme(theme);
|
||||||
@@ -704,6 +715,9 @@ export class ThemeManager {
|
|||||||
if (defaultColour) {
|
if (defaultColour) {
|
||||||
settingsState.selectedColor = defaultColour;
|
settingsState.selectedColor = defaultColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);
|
||||||
|
void updateAllColors();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error previewing theme:", error);
|
console.error("[ThemeManager] Error previewing theme:", error);
|
||||||
}
|
}
|
||||||
@@ -778,6 +792,9 @@ export class ThemeManager {
|
|||||||
if (!theme.webURL && theme.defaultColour) {
|
if (!theme.webURL && theme.defaultColour) {
|
||||||
settingsState.selectedColor = theme.defaultColour;
|
settingsState.selectedColor = theme.defaultColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);
|
||||||
|
void updateAllColors();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error updating theme preview:", error);
|
console.error("[ThemeManager] Error updating theme preview:", error);
|
||||||
}
|
}
|
||||||
@@ -815,6 +832,8 @@ export class ThemeManager {
|
|||||||
this.previewStyleElement = null;
|
this.previewStyleElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearCustomThemeAdaptiveCssVariables();
|
||||||
|
|
||||||
// Restore original settings
|
// Restore original settings
|
||||||
const storedColor = localStorage.getItem("originalPreviewColor");
|
const storedColor = localStorage.getItem("originalPreviewColor");
|
||||||
|
|
||||||
@@ -844,6 +863,8 @@ export class ThemeManager {
|
|||||||
settingsState.DarkMode = this.originalPreviewTheme;
|
settingsState.DarkMode = this.originalPreviewTheme;
|
||||||
this.originalPreviewTheme = null;
|
this.originalPreviewTheme = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateAllColors();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ThemeManager] Error clearing preview:", error);
|
console.error("[ThemeManager] Error clearing preview:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { lightenAndPaleColor } from "./lightenAndPaleColor";
|
|||||||
import ColorLuminance from "./ColorLuminance";
|
import ColorLuminance from "./ColorLuminance";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
||||||
|
import { getCustomThemeAdaptiveCssVariables } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
|
||||||
|
|
||||||
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||||
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
|
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
|
||||||
@@ -127,6 +128,12 @@ function applyColorsWith(selectedColor: string) {
|
|||||||
// Apply all the properties
|
// Apply all the properties
|
||||||
applyProperties({ ...commonProps, ...modeProps, ...dynamicProps });
|
applyProperties({ ...commonProps, ...modeProps, ...dynamicProps });
|
||||||
|
|
||||||
|
if (settingsState.selectedTheme) {
|
||||||
|
for (const name of getCustomThemeAdaptiveCssVariables()) {
|
||||||
|
setCSSVar(name, selectedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let alliframes = document.getElementsByTagName("iframe");
|
let alliframes = document.getElementsByTagName("iframe");
|
||||||
|
|
||||||
for (let i = 0; i < alliframes.length; i++) {
|
for (let i = 0; i < alliframes.length; i++) {
|
||||||
|
|||||||
@@ -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<string>();
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export class StorageChangeHandler {
|
|||||||
settingsState.register("adaptiveThemeColourTransition", () =>
|
settingsState.register("adaptiveThemeColourTransition", () =>
|
||||||
void updateAllColors(),
|
void updateAllColors(),
|
||||||
);
|
);
|
||||||
|
settingsState.register("selectedTheme", () => void updateAllColors());
|
||||||
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
|
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
|
||||||
settingsState.register("onoff", this.handleOnOffChange.bind(this));
|
settingsState.register("onoff", this.handleOnOffChange.bind(this));
|
||||||
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
|
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export type CustomTheme = {
|
|||||||
webURL?: string;
|
webURL?: string;
|
||||||
selectedColor?: string;
|
selectedColor?: string;
|
||||||
forceDark?: boolean;
|
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 & {
|
export type LoadedCustomTheme = CustomTheme & {
|
||||||
|
|||||||
Reference in New Issue
Block a user