import tailwindStyles from "@/interface/index.css?inline"; import pluginStyles from "./styles.css?inline"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { mount, unmount } from "svelte"; import GradeAnalyticsPage from "./GradeAnalyticsPage.svelte"; type ThemeSettingKey = | "selectedColor" | "DarkMode" | "adaptiveThemeColour" | "adaptiveThemeGradient" | "selectedTheme"; type ThemeListenerRegistration = { key: ThemeSettingKey; listener: () => void; }; let currentApp: ReturnType | null = null; let shadowHost: HTMLElement | null = null; let analyticsRoot: HTMLElement | null = null; let darkModeObserver: MutationObserver | null = null; let themeStyleObserver: MutationObserver | null = null; let themeListeners: ThemeListenerRegistration[] = []; const ANALYTICS_STACKING_STYLE_ID = "bsplus-analytics-stacking-styles"; /** Light-DOM stacking scope so toolbar/dropdown z-index cannot paint over ExtensionPopup. */ function ensureAnalyticsStackingScope() { if (document.getElementById(ANALYTICS_STACKING_STYLE_ID)) return; const style = document.createElement("style"); style.id = ANALYTICS_STACKING_STYLE_ID; style.textContent = ` #analytics-view-container, .bsplus-analytics-container, .bsplus-analytics-host { position: relative; z-index: 0; isolation: isolate; } `; document.head.appendChild(style); } const THEME_CSS_VARS = [ "--better-main", "--better-pale", "--better-light", "--text-color", "--background-primary", "--background-secondary", "--text-primary", "--theme-offset-bg", "--better-sub", ] as const; const ACCENT_CSS_VARS = [ "--better-main", "--accent-color-value", "--accentColor", "--colour-betterseqta-blue", ] as const; /** Resolve a solid colour for charts (gradients → first stop). */ function extractSolidColor(value: string): string | null { const trimmed = value.trim(); if (!trimmed || trimmed === "initial") return null; if ( trimmed.startsWith("#") || trimmed.startsWith("rgb") || trimmed.startsWith("hsl") ) { return trimmed; } if (trimmed.includes("gradient")) { const match = trimmed.match( /#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgba?\([^)]+\)/i, ); return match?.[0] ?? null; } return null; } const THEME_ACCENT_OVERRIDES: Record = { "bb0aaf40-55ef-40f7-bc64-93b67ef96c01": "#4ade80", }; function resolvePageAccentColor(): string { const themeId = settingsState.selectedTheme; if (themeId && themeId in THEME_ACCENT_OVERRIDES) { return THEME_ACCENT_OVERRIDES[themeId]; } const computed = getComputedStyle(document.documentElement); for (const name of ACCENT_CSS_VARS) { const solid = extractSolidColor(computed.getPropertyValue(name)); if (solid) return solid; } const fromSettings = settingsState.selectedColor?.trim(); if (fromSettings) { const solid = extractSolidColor(fromSettings); if (solid) return solid; } return "#007bff"; } function syncThemeFromPage(target: HTMLElement) { const computed = getComputedStyle(document.documentElement); for (const name of THEME_CSS_VARS) { let value = computed.getPropertyValue(name).trim(); value = document.documentElement.style.getPropertyValue(name).trim(); if (value) { target.style.setProperty(name, value); } } const accent = resolvePageAccentColor(); target.style.setProperty("--bsplus-analytics-accent", accent); target.style.setProperty("--better-main", accent); target.classList.toggle( "dark", document.documentElement.classList.contains("dark"), ); } function syncThemeToAnalyticsUi() { if (shadowHost) syncThemeFromPage(shadowHost); if (analyticsRoot) syncThemeFromPage(analyticsRoot); } function clearThemeListeners() { for (const { key, listener } of themeListeners) { settingsState.unregister(key, listener); } themeListeners = []; } function watchThemeChanges() { clearThemeListeners(); const keys: ThemeSettingKey[] = [ "selectedColor", "DarkMode", "adaptiveThemeColour", "adaptiveThemeGradient", "selectedTheme", ]; const listener = () => syncThemeToAnalyticsUi(); for (const key of keys) { settingsState.register(key, listener); themeListeners.push({ key, listener }); } themeStyleObserver?.disconnect(); themeStyleObserver = new MutationObserver(() => syncThemeToAnalyticsUi()); themeStyleObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["style", "class"], }); } function teardown() { clearThemeListeners(); themeStyleObserver?.disconnect(); themeStyleObserver = null; if (currentApp) { unmount(currentApp); currentApp = null; } darkModeObserver?.disconnect(); darkModeObserver = null; shadowHost?.remove(); shadowHost = null; analyticsRoot = null; } export function renderAnalyticsPage(container: HTMLElement) { teardown(); ensureAnalyticsStackingScope(); container.innerHTML = ""; container.className = "bsplus-analytics-container"; shadowHost = document.createElement("div"); shadowHost.className = "bsplus-analytics-host"; container.appendChild(shadowHost); const shadow = shadowHost.attachShadow({ mode: "open" }); const styleElement = document.createElement("style"); styleElement.textContent = `${tailwindStyles}\n${pluginStyles}`; shadow.appendChild(styleElement); analyticsRoot = document.createElement("div"); analyticsRoot.className = "bsplus-analytics-root"; syncThemeToAnalyticsUi(); shadow.appendChild(analyticsRoot); watchThemeChanges(); darkModeObserver = new MutationObserver(() => syncThemeToAnalyticsUi()); darkModeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }); currentApp = mount(GradeAnalyticsPage, { target: analyticsRoot }); } export function unmountAnalyticsPage() { teardown(); }