diff --git a/package.json b/package.json index 6695659c..8f0615e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betterseqtaplus", - "version": "3.6.5", + "version": "3.6.6", "type": "module", "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", diff --git a/src/background.ts b/src/background.ts index b98b5514..074a4138 100644 --- a/src/background.ts +++ b/src/background.ts @@ -551,6 +551,76 @@ async function migrateGlobalSearchDefaultsFor365Upgrade( } } +/** One-time reset for 3.6.6: re-enable Theme of the Month for existing users. */ +const THEME_OF_THE_MONTH_RESET_VERSION = "3.6.6"; + +async function resetThemeOfTheMonthDisabledFor366Upgrade( + previousVersion: string, +): Promise { + try { + const currRaw = browser.runtime.getManifest().version; + const prev = semver.coerce(previousVersion); + const curr = semver.coerce(currRaw); + if ( + prev == null || + curr == null || + semver.lt(curr, THEME_OF_THE_MONTH_RESET_VERSION) || + !semver.lt(prev, THEME_OF_THE_MONTH_RESET_VERSION) + ) { + return; + } + + await browser.storage.local.set({ + themeOfTheMonthDisabled: false, + themeOfTheMonthLastSeenId: undefined, + }); + + console.info( + `[BetterSEQTA+] Migration ${THEME_OF_THE_MONTH_RESET_VERSION}: Theme of the Month re-enabled (from ${previousVersion}).`, + ); + } catch (e) { + console.warn( + "[BetterSEQTA+] Theme of the Month 3.6.6 reset migration failed:", + e, + ); + } +} + +/** 3.7.0: Close no longer marks entries seen — clear legacy dismissal keys. */ +const THEME_OF_THE_MONTH_RELOAD_VERSION = "3.7.0"; + +async function resetThemeOfTheMonthDismissalFor370Upgrade( + previousVersion: string, +): Promise { + try { + const currRaw = browser.runtime.getManifest().version; + const prev = semver.coerce(previousVersion); + const curr = semver.coerce(currRaw); + if ( + prev == null || + curr == null || + semver.lt(curr, THEME_OF_THE_MONTH_RELOAD_VERSION) || + !semver.lt(prev, THEME_OF_THE_MONTH_RELOAD_VERSION) + ) { + return; + } + + await browser.storage.local.set({ + themeOfTheMonthLastSeenId: undefined, + themeOfTheMonthDismissedMonth: undefined, + }); + + console.info( + `[BetterSEQTA+] Migration ${THEME_OF_THE_MONTH_RELOAD_VERSION}: Theme of the Month shows again until dismissed for the month (from ${previousVersion}).`, + ); + } catch (e) { + console.warn( + "[BetterSEQTA+] Theme of the Month 3.7.0 dismissal migration failed:", + e, + ); + } +} + browser.runtime.onInstalled.addListener(function (event) { browser.storage.local.remove(["justupdated"]); browser.storage.local.remove(["data"]); @@ -561,6 +631,8 @@ browser.runtime.onInstalled.addListener(function (event) { if (event.reason === "update" && event.previousVersion) { void migrateGlobalSearchDefaultsFor365Upgrade(event.previousVersion); + void resetThemeOfTheMonthDisabledFor366Upgrade(event.previousVersion); + void resetThemeOfTheMonthDismissalFor370Upgrade(event.previousVersion); } }); diff --git a/src/css/injected.scss b/src/css/injected.scss index 57f7fc64..a1580330 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -455,6 +455,58 @@ ul.magicDelete > li.deleting { top: 71.5px; margin-top: -2px; } + +/* Drill-in stack: only the current list + folder header stay clickable. + Class is toggled by updateSidebarAccessibility (never touches aria-hidden). */ +#menu .bsplus-sidebar-offscreen, +#menu .bsplus-sidebar-offscreen * { + pointer-events: none !important; + user-select: none !important; +} + +#menu > ul > .bsplus-sidebar-offscreen:not(.hasChildren.active) { + position: absolute !important; + left: -10000px !important; + width: 1px !important; + height: 1px !important; + margin: 0 !important; + padding: 0 !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + opacity: 0 !important; +} + +#menu .sub .bsplus-sidebar-offscreen:not(.hasChildren.active) { + visibility: hidden !important; + position: absolute !important; + left: -10000px !important; + width: 1px !important; + height: 1px !important; + margin: 0 !important; + padding: 0 !important; + opacity: 0 !important; +} + +/* Only the frontmost open .sub panel receives pointer events */ +#menu .sub { + pointer-events: none; +} + +#menu li.hasChildren.active > .sub { + pointer-events: auto; +} + +#menu li.hasChildren.active > .sub:has(.hasChildren.active) { + pointer-events: none !important; +} + +#menu li.hasChildren.active .hasChildren.active > .sub { + pointer-events: auto !important; +} + +#menu:has(> ul > li.hasChildren.active) > ul > li:not(.hasChildren.active) { + pointer-events: none !important; +} #menu section > label { align-items: center; box-sizing: border-box; @@ -2326,6 +2378,10 @@ blurred { height: 64px; cursor: pointer; } +/* While a drill-in submenu is open, don't steal clicks meant for folder rows. */ +#menu:has(li.hasChildren.active) > .icon-cover { + pointer-events: none; +} .uiSlidePane > .pane > .header button { color: var(--text-color) !important; } @@ -3753,20 +3809,77 @@ div.day-empty { pointer-events: none; animation: themeOfTheMonthCardOut 0.18s ease-in forwards; } -.themeOfTheMonthCardClose { - position: absolute !important; - top: 4px !important; - right: 4px !important; - z-index: 2; - width: 32px; - height: 32px; - border: 1px solid rgba(255, 255, 255, 0.22); - border-radius: 16px !important; - background: rgba(0, 0, 0, 0.42); - color: white; +.themeOfTheMonthCardConfirm { + position: absolute; + inset: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + border-radius: inherit; + background: color-mix(in srgb, var(--background-primary) 88%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + opacity: 0; + pointer-events: none; + transition: opacity 0.16s ease; +} +.themeOfTheMonthCardConfirm[hidden] { + display: none; +} +.themeOfTheMonthCardConfirmVisible { + opacity: 1; + pointer-events: auto; +} +.themeOfTheMonthCardConfirmInner { + width: 100%; + text-align: center; +} +.themeOfTheMonthCardConfirmInner h3 { + margin: 0 0 6px; + font-size: 1rem; + line-height: 1.2; +} +.themeOfTheMonthCardConfirmInner p { + margin: 0 0 14px; + font-size: 0.86rem; + line-height: 1.4; + color: color-mix(in srgb, var(--text-primary) 78%, transparent); +} +.themeOfTheMonthCardConfirmActions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} +.themeOfTheMonthCardConfirmCancel, +.themeOfTheMonthCardConfirmAccept { + appearance: none; + border: none; cursor: pointer; - font-size: 1.35rem; - line-height: 1; + border-radius: 9999px; + padding: 0.5rem 0.85rem; + font-size: 0.84rem; + font-weight: 700; + transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease; +} +.themeOfTheMonthCardConfirmCancel { + background: color-mix(in srgb, var(--text-primary) 10%, transparent); + color: var(--text-primary); +} +.themeOfTheMonthCardConfirmAccept { + background: var(--better-pri, #6366f1); + color: white; +} +.themeOfTheMonthCardConfirmCancel:hover, +.themeOfTheMonthCardConfirmAccept:hover { + filter: brightness(1.08); + transform: translateY(-1px); +} +.themeOfTheMonthCardConfirmCancel:active, +.themeOfTheMonthCardConfirmAccept:active { + transform: translateY(0); } .themeOfTheMonthCardImage { display: block; @@ -3805,11 +3918,34 @@ div.day-empty { .themeOfTheMonthCardActions { display: flex; flex-wrap: wrap; - justify-content: flex-end; - gap: 8px; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.themeOfTheMonthCardActionsStart { + display: flex; + flex-wrap: wrap; + align-items: center; +} +.themeOfTheMonthCardActionsEnd { + display: inline-flex; + flex-wrap: nowrap; + align-items: stretch; + margin-left: auto; + padding: 3px; + gap: 0; + overflow: hidden; + border-radius: 9999px; + background: color-mix( + in srgb, + var(--background-secondary, var(--text-primary)) 28%, + var(--background-primary) + ); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary) 8%, transparent); } .themeOfTheMonthCardPrimary, -.themeOfTheMonthCardSecondary { +.themeOfTheMonthCardSecondary, +.themeOfTheMonthCardDontShow { appearance: none; border: none; cursor: pointer; @@ -3817,23 +3953,49 @@ div.day-empty { padding: 0.58rem 0.9rem; font-size: 0.86rem; font-weight: 700; - transition: transform 0.15s ease, filter 0.15s ease, background 0.15s ease; + transition: background 0.15s ease, color 0.15s ease; } .themeOfTheMonthCardPrimary { background: var(--better-pri, #6366f1); color: white; } -.themeOfTheMonthCardSecondary { - background: color-mix(in srgb, var(--text-primary) 10%, transparent); +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow { + padding: 0.5rem 0.8rem; + font-size: 0.8rem; + font-weight: 600; + border: none !important; + border-radius: 9999px !important; + background: transparent !important; + box-shadow: none !important; + filter: none !important; + transform: none !important; +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary { color: var(--text-primary); } -.themeOfTheMonthCardPrimary:hover, -.themeOfTheMonthCardSecondary:hover { +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow { + color: color-mix(in srgb, var(--text-primary) 58%, transparent); +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:hover, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:focus-visible, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:hover, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:focus-visible { + background: color-mix(in srgb, var(--text-primary) 10%, transparent) !important; + border-radius: 9999px !important; + filter: none !important; + transform: none !important; +} +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardSecondary:active, +#theme-of-the-month-card .themeOfTheMonthCardActionsEnd .themeOfTheMonthCardDontShow:active { + background: color-mix(in srgb, var(--text-primary) 14%, transparent) !important; + border-radius: 9999px !important; +} +.themeOfTheMonthCardPrimary:hover { filter: brightness(1.08); transform: translateY(-1px); } -.themeOfTheMonthCardPrimary:active, -.themeOfTheMonthCardSecondary:active { +.themeOfTheMonthCardPrimary:active { transform: translateY(0); } @keyframes themeOfTheMonthCardIn { diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 7bc7ecc9..6140c02f 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -423,6 +423,18 @@ {/each} {/if} + {#if plugin.pluginId === 'global-search'} + {@render Setting({ + title: "Theme of the Month", + description: "Show the monthly featured theme popup when a new entry is available", + id: 15, + Component: Switch, + props: { + state: !($settingsState.themeOfTheMonthDisabled ?? false), + onChange: (isOn: boolean) => settingsState.themeOfTheMonthDisabled = !isOn + } + })} + {/if} {/each} diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index ccaeddf5..492315df 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, + type ThemeDomSpec, + type ThemeScriptSpec, + validateThemeDom, + validateThemeScript, +} 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 `