From 3d1320277950a27ed38efc428898ad1f52360d3c Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Fri, 29 May 2026 00:44:02 +0930 Subject: [PATCH 1/3] feat: handlers for night city theme's features --- src/plugins/built-in/themes/theme-manager.ts | 74 +- src/plugins/built-in/themes/theme-runtime.ts | 714 +++++++++++++++++++ src/types/CustomThemes.ts | 12 + 3 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 src/plugins/built-in/themes/theme-runtime.ts diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index ccaeddf5..3ab5d46d 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -17,6 +17,15 @@ import { clearCustomThemeAdaptiveCssVariables, setCustomThemeAdaptiveCssVariables, } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; +import { + clearThemeRuntime, + injectThemeDom, + runThemeScript, + validateThemeDom, + validateThemeScript, + type ThemeDomSpec, + type ThemeScriptSpec, +} from "./theme-runtime"; type ThemeContent = { id: string; @@ -31,6 +40,8 @@ type ThemeContent = { forceDark?: boolean; adaptiveCssVariables?: string[]; images?: { id: string; variableName: string; data: string }[]; // data: base64 + themeScript?: ThemeScriptSpec; + themeDom?: ThemeDomSpec; }; export type InstallThemeMeta = { @@ -53,6 +64,7 @@ export class ThemeManager { private imageUrlCache: Map = new Map(); private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 }; private storeUpdateCheckRunning = false; + private headObserver: MutationObserver | null = null; private constructor() { console.debug("[ThemeManager] Initializing..."); @@ -307,6 +319,13 @@ export class ThemeManager { private async applyTheme(theme: CustomTheme): Promise { console.debug("[ThemeManager] Applying theme:", theme.name); try { + // Run the theme script BEFORE injecting CustomCSS so any state the + // script publishes (e.g. `data-city-state` and `--city-sky-color` for + // Noir City) is already on when the new CSS rules paint. + // Otherwise the CSS lands with var() unresolved and the page flashes + // its previous state before snapping to the right colour. + runThemeScript(theme.themeScript); + // Apply custom CSS if (theme.CustomCSS) { console.debug("[ThemeManager] Applying custom CSS"); @@ -348,6 +367,8 @@ export class ThemeManager { } setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []); + + injectThemeDom(theme.themeDom); } catch (error) { console.error("[ThemeManager] Error applying theme:", error); } @@ -379,6 +400,13 @@ export class ThemeManager { ): Promise { console.debug("[ThemeManager] Removing theme:", theme.name); try { + clearThemeRuntime(); + + // Disconnect the head observer BEFORE removing the style element, + // otherwise the removal fires the observer and it would no-op only + // because the style is already gone — wasted work, but harmless. + this.disconnectStyleObserver(); + // Remove custom CSS if (this.styleElement) { console.debug("[ThemeManager] Removing custom CSS"); @@ -448,7 +476,13 @@ export class ThemeManager { } /** - * Apply custom CSS to the document + * Apply custom CSS to the document. The `