feat: Themes can adapt to colour

This commit is contained in:
2026-04-06 14:11:19 +09:30
parent f667ff9e9b
commit e657152e3f
6 changed files with 98 additions and 2 deletions
+27 -2
View File
@@ -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 &quot;Adaptive theme colour&quot; 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&#10;--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);
} }
+7
View File
@@ -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));
+2
View File
@@ -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 & {