From f667ff9e9b74555f03dfe760ad773681e6ca7790 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Mon, 6 Apr 2026 13:39:25 +0930 Subject: [PATCH 1/4] feat: Smooth change in colour, no hard cut Added option smoothing on colour change so there is no hard cut made when switching subjects --- package.json | 2 +- src/background.ts | 1 + src/interface/pages/settings/general.svelte | 12 ++ src/seqta/ui/colors/Manager.ts | 134 +++++++++++++++++++- src/seqta/utils/listeners/StorageChanges.ts | 3 + src/types/storage.ts | 1 + 6 files changed, 147 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 175c8755..d175d5ff 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "betterseqtaplus", "version": "3.5.3", "type": "module", - "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!", + "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development and add heaps more features!", "browserslist": "> 0.5%, last 2 versions, not dead", "scripts": { "autoaudit": "npm audit && npm audit fix && npm run build", diff --git a/src/background.ts b/src/background.ts index e5959f81..6a897ffb 100644 --- a/src/background.ts +++ b/src/background.ts @@ -331,6 +331,7 @@ function getDefaultValues(): SettingsState { iconOnlySidebar: false, adaptiveThemeColour: false, adaptiveThemeGradient: false, + adaptiveThemeColourTransition: true, }; } diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 09e6d1cd..1e6e414f 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -252,6 +252,18 @@ /> +
+
+

Smooth colour transition

+

Ease between class/subject colours when navigating instead of switching instantly

+
+
+ settingsState.adaptiveThemeColourTransition = isOn} + /> +
+
{/if} diff --git a/src/seqta/ui/colors/Manager.ts b/src/seqta/ui/colors/Manager.ts index 209c74ba..f035fe12 100644 --- a/src/seqta/ui/colors/Manager.ts +++ b/src/seqta/ui/colors/Manager.ts @@ -9,12 +9,80 @@ import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour"; import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; +const ADAPTIVE_THEME_TRANSITION_MS = 400; + +let colorTransitionRafId: number | null = null; +let lastInterpolatedHex: string | null = null; + // Helper functions const setCSSVar = (varName: any, value: any) => document.documentElement.style.setProperty(varName, value); const applyProperties = (props: any) => Object.entries(props).forEach(([key, value]) => setCSSVar(key, value)); +function easeInOutCubic(t: number): number { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +/** Best-effort parse of a single sRGB hex from a colour string (hex, rgb, or gradient). */ +function parseRepresentativeHex(s: string): string | null { + if (!s || !s.trim()) return null; + const trimmed = s.trim(); + try { + return Color(trimmed).hex(); + } catch { + // continue + } + if (trimmed.includes("gradient")) { + const regex = + /#[0-9a-fA-F]{6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)/gi; + const stops = trimmed.match(regex); + if (stops?.length) { + try { + return Color(stops[0]).hex(); + } catch { + // continue + } + } + } + const hexMatch = trimmed.match(/#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})\b/); + if (hexMatch) { + try { + return Color(hexMatch[0]).hex(); + } catch { + // continue + } + } + const rgbaMatch = trimmed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); + if (rgbaMatch) { + try { + return Color.rgb( + Number(rgbaMatch[1]), + Number(rgbaMatch[2]), + Number(rgbaMatch[3]), + ).hex(); + } catch { + // continue + } + } + return null; +} + +function getFromHex(): string | null { + const fromComputed = parseRepresentativeHex( + getComputedStyle(document.documentElement).getPropertyValue("--better-main").trim(), + ); + if (fromComputed) return fromComputed; + return lastInterpolatedHex; +} + +function cancelColorTransition() { + if (colorTransitionRafId !== null) { + cancelAnimationFrame(colorTransitionRafId); + colorTransitionRafId = null; + } +} + function applyColorsWith(selectedColor: string) { if (settingsState.transparencyEffects) { document.documentElement.classList.add("transparencyEffects"); @@ -89,15 +157,71 @@ export async function updateAllColors() { ? settingsState.selectedColor : "#007bff"; + let adaptiveHex: string | null = null; + if (settingsState.adaptiveThemeColour) { const adaptiveColor = await getAdaptiveColour(); if (adaptiveColor) { - effectiveColor = - settingsState.adaptiveThemeGradient - ? toSoftGradient(adaptiveColor) - : adaptiveColor; + adaptiveHex = adaptiveColor; + effectiveColor = settingsState.adaptiveThemeGradient + ? toSoftGradient(adaptiveColor) + : adaptiveColor; } } - applyColorsWith(effectiveColor); + const baseSelected = + settingsState.selectedColor !== "" ? settingsState.selectedColor : "#007bff"; + const toHex = + adaptiveHex ?? parseRepresentativeHex(baseSelected); + + const shouldAnimate = + settingsState.adaptiveThemeColour && + (settingsState.adaptiveThemeColourTransition ?? true) && + !!toHex; + + const applyImmediate = () => { + cancelColorTransition(); + applyColorsWith(effectiveColor); + if (toHex) lastInterpolatedHex = toHex; + }; + + if (!shouldAnimate) { + applyImmediate(); + return; + } + + const fromHex = getFromHex(); + + if (!fromHex || !toHex || fromHex === toHex) { + applyImmediate(); + return; + } + + const useSoftGradientOnFrames = + !!adaptiveHex && !!settingsState.adaptiveThemeGradient; + + cancelColorTransition(); + + const start = performance.now(); + + const step = (now: number) => { + const elapsed = now - start; + const t = Math.min(1, elapsed / ADAPTIVE_THEME_TRANSITION_MS); + const eased = easeInOutCubic(t); + const interpolatedHex = Color(fromHex).mix(Color(toHex), eased).hex(); + const display = useSoftGradientOnFrames + ? toSoftGradient(interpolatedHex) + : interpolatedHex; + applyColorsWith(display); + + if (t < 1) { + colorTransitionRafId = requestAnimationFrame(step); + } else { + colorTransitionRafId = null; + applyColorsWith(effectiveColor); + lastInterpolatedHex = toHex; + } + }; + + colorTransitionRafId = requestAnimationFrame(step); } diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 15d495c9..7e178379 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -17,6 +17,9 @@ export class StorageChangeHandler { settingsState.register("selectedColor", () => void updateAllColors()); settingsState.register("adaptiveThemeColour", () => void updateAllColors()); settingsState.register("adaptiveThemeGradient", () => void updateAllColors()); + settingsState.register("adaptiveThemeColourTransition", () => + 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/storage.ts b/src/types/storage.ts index dc683f2c..56dda410 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -43,6 +43,7 @@ export interface SettingsState { iconOnlySidebar?: boolean; adaptiveThemeColour?: boolean; adaptiveThemeGradient?: boolean; + adaptiveThemeColourTransition?: boolean; // depreciated keys animatedbk: boolean; From e657152e3fb98d99c329614c7ee8b35b88690ac0 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Mon, 6 Apr 2026 14:11:19 +0930 Subject: [PATCH 2/4] feat: Themes can adapt to colour --- src/interface/pages/themeCreator.svelte | 29 +++++++++++++- src/plugins/built-in/themes/theme-manager.ts | 21 ++++++++++ src/seqta/ui/colors/Manager.ts | 7 ++++ .../ui/colors/customThemeAdaptiveBindings.ts | 40 +++++++++++++++++++ src/seqta/utils/listeners/StorageChanges.ts | 1 + src/types/CustomThemes.ts | 2 + 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/seqta/ui/colors/customThemeAdaptiveBindings.ts 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 & { From ac1ee702aec2cc98fc13afb66e723703b3463e69 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Mon, 6 Apr 2026 14:24:08 +0930 Subject: [PATCH 3/4] quick fix to forced mode as well --- src/interface/pages/themeCreator.svelte | 31 ++++++++++++++--- src/plugins/built-in/themes/theme-manager.ts | 35 ++++++++++++++------ src/types/CustomThemes.ts | 20 +++++++++++ 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index a51b7e58..d8da6b95 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -4,7 +4,10 @@ import { slide } from 'svelte/transition'; import { fade } from 'svelte/transition'; - import { type LoadedCustomTheme } from '@/types/CustomThemes' + import { + type LoadedCustomTheme, + shouldForceThemeAppearance, + } from '@/types/CustomThemes' import { settingsState } from '@/seqta/utils/listeners/SettingsState' @@ -40,6 +43,7 @@ coverImage: null, isEditable: true, hideThemeName: false, + forceTheme: undefined, forceDark: undefined, adaptiveCssVariables: [], }) @@ -84,6 +88,9 @@ theme = { ...loadedTheme, adaptiveCssVariables: loadedTheme.adaptiveCssVariables ?? [], + forceTheme: + loadedTheme.forceTheme ?? + (loadedTheme.forceDark !== undefined ? true : undefined), } themeLoaded = true } else { @@ -119,6 +126,13 @@ })) themeClone.coverImage = theme.coverImage + if (shouldForceThemeAppearance(themeClone)) { + themeClone.forceTheme = true; + } else { + themeClone.forceTheme = false; + themeClone.forceDark = undefined; + } + themeManager.clearPreview(); await themeManager.saveTheme(themeClone); await themeManager.setTheme(themeClone.id); @@ -357,21 +371,28 @@ title: 'Force Theme', description: 'Force users to use either dark or light mode', props: { - state: theme.forceDark !== undefined, - onChange: (value: boolean) => theme = { ...theme, forceDark: value ? false : undefined } + state: shouldForceThemeAppearance(theme), + onChange: (value: boolean) => { + if (value) { + theme = { ...theme, forceTheme: true, forceDark: false }; + } else { + theme = { ...theme, forceTheme: false, forceDark: undefined }; + } + } } }, { type: 'conditional', props: { - condition: theme.forceDark !== undefined, + condition: shouldForceThemeAppearance(theme), children: { type: 'lightDarkToggle', title: 'Mode', description: 'Choose whether to force light or dark mode', props: { state: theme.forceDark === true, - onChange: (value: boolean) => theme = { ...theme, forceDark: value } + onChange: (value: boolean) => + (theme = { ...theme, forceDark: value, forceTheme: true }) } } } diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index cd499c3a..64ac26c8 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -1,6 +1,11 @@ import localforage from "localforage"; import browser from "webextension-polyfill"; -import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes"; +import { + type CustomTheme, + type LoadedCustomTheme, + getForcedDarkMode, + shouldForceThemeAppearance, +} from "@/types/CustomThemes"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import debounce from "@/seqta/utils/debounce"; import { updateAllColors } from "@/seqta/ui/colors/Manager"; @@ -19,6 +24,7 @@ type ThemeContent = { CustomCSS?: string; hideThemeName?: boolean; forceDark?: boolean; + forceTheme?: boolean; adaptiveCssVariables?: string[]; images: { id: string; variableName: string; data: string }[]; // data: base64 }; @@ -215,7 +221,7 @@ export class ThemeManager { console.debug("[ThemeManager] Storing original settings"); settingsState.originalSelectedColor = settingsState.selectedColor; - if (theme.forceDark) { + if (shouldForceThemeAppearance(theme)) { settingsState.originalDarkMode = settingsState.DarkMode; } } @@ -277,9 +283,10 @@ export class ThemeManager { } // Apply theme settings - if (theme.forceDark !== undefined) { - console.debug("[ThemeManager] Setting dark mode:", theme.forceDark); - settingsState.DarkMode = theme.forceDark; + if (shouldForceThemeAppearance(theme)) { + const dark = getForcedDarkMode(theme); + console.debug("[ThemeManager] Setting dark mode:", dark); + settingsState.DarkMode = dark; } // Use the stored selected color if available, otherwise use the default @@ -595,6 +602,12 @@ export class ThemeManager { isEditable: false, hideThemeName: themeData.hideThemeName ?? false, forceDark: themeData.forceDark, + forceTheme: + themeData.forceTheme !== undefined + ? themeData.forceTheme + : themeData.forceDark !== undefined + ? true + : undefined, adaptiveCssVariables: themeData.adaptiveCssVariables, }; @@ -662,7 +675,7 @@ export class ThemeManager { public async previewTheme(theme: LoadedCustomTheme): Promise { console.debug("[ThemeManager] Previewing theme:", theme.name); try { - const { CustomCSS, CustomImages, defaultColour, forceDark } = theme; + const { CustomCSS, CustomImages, defaultColour } = theme; // Store original settings only if this is a new theme if (!theme.webURL) { @@ -708,8 +721,8 @@ export class ThemeManager { this.previousImageVariableNames = newImageVariableNames; // Apply theme settings - if (forceDark !== undefined) { - settingsState.DarkMode = forceDark; + if (shouldForceThemeAppearance(theme)) { + settingsState.DarkMode = getForcedDarkMode(theme); } if (defaultColour) { @@ -783,9 +796,9 @@ export class ThemeManager { this.previousImageVariableNames = newImageVariableNames; } - // Always apply dark mode setting - if (theme.forceDark !== undefined) { - settingsState.DarkMode = theme.forceDark; + // Always apply dark mode setting when theme forces appearance + if (shouldForceThemeAppearance(theme as CustomTheme)) { + settingsState.DarkMode = getForcedDarkMode(theme as CustomTheme); } // Only apply color if this is a new theme diff --git a/src/types/CustomThemes.ts b/src/types/CustomThemes.ts index 92498428..6ff7def4 100644 --- a/src/types/CustomThemes.ts +++ b/src/types/CustomThemes.ts @@ -12,6 +12,11 @@ export type CustomTheme = { hideThemeName: boolean; webURL?: string; selectedColor?: string; + /** + * When true, the theme forces light/dark via `forceDark` (`false` = light, `true` = dark). + * When false/omitted, use legacy rule: `forceDark !== undefined` still means "force" for old JSON. + */ + forceTheme?: 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[]; @@ -39,3 +44,18 @@ export type ThemeList = { themes: CustomTheme[]; selectedTheme: string; }; + +/** Whether the theme forces appearance (light vs dark). */ +export function shouldForceThemeAppearance(theme: { + forceTheme?: boolean; + forceDark?: boolean; +}): boolean { + if (theme.forceTheme === true) return true; + if (theme.forceTheme === false) return false; + return theme.forceDark !== undefined; +} + +/** Resolved forced dark mode when forcing is active. */ +export function getForcedDarkMode(theme: { forceDark?: boolean }): boolean { + return theme.forceDark === true; +} From 8123c5dd332ec759f96cb89966b112ff9c45e6fb Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Mon, 6 Apr 2026 04:55:44 +0000 Subject: [PATCH 4/4] [CodeFactor] Apply fixes --- src/plugins/built-in/themes/theme-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 64ac26c8..7e3b2192 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -2,8 +2,8 @@ import localforage from "localforage"; import browser from "webextension-polyfill"; import { type CustomTheme, - type LoadedCustomTheme, getForcedDarkMode, + type LoadedCustomTheme, shouldForceThemeAppearance, } from "@/types/CustomThemes"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";