diff --git a/src/background.ts b/src/background.ts index 756a9812..775778f7 100644 --- a/src/background.ts +++ b/src/background.ts @@ -495,6 +495,7 @@ function getDefaultValues(): SettingsState { adaptiveThemeColour: false, adaptiveThemeGradient: false, adaptiveThemeColourTransition: true, + themeOfTheMonthDisabled: false, autoCloudSettingsSync: true, }; } diff --git a/src/css/injected.scss b/src/css/injected.scss index c01c3590..57f7fc64 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -3726,45 +3726,136 @@ div.day-empty { color: var(--text-primary); } -.whatsnewHeader.themeOfTheMonthHeader { - height: auto; - min-height: unset; +.themeOfTheMonthCard { + position: fixed; + right: max(18px, env(safe-area-inset-right)); + bottom: max(18px, env(safe-area-inset-bottom)); + z-index: 48; + width: min(360px, calc(100vw - 36px)); + overflow: visible; + border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent); + border-radius: 20px; + background: var(--background-primary); + color: var(--text-primary); + box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35); + animation: themeOfTheMonthCardIn 0.24s ease-out; } -.whatsnewHeader.themeOfTheMonthHeader h1 { +.themeOfTheMonthCard::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + overflow: hidden; + border-radius: inherit; + background: inherit; +} +.themeOfTheMonthCardClosing { + 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; + cursor: pointer; + font-size: 1.35rem; + line-height: 1; +} +.themeOfTheMonthCardImage { + display: block; + width: 100%; + height: 150px; + margin: 0; + border-radius: 20px 20px 0 0; + object-fit: cover; +} +.themeOfTheMonthCardBody { + padding: 14px 16px 16px; +} +.themeOfTheMonthCardEyebrow { + margin: 0 0 6px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: color-mix(in srgb, var(--better-pri, #6366f1) 82%, var(--text-primary) 18%); +} +.themeOfTheMonthCard h2 { + margin: 0; + font-size: 1.2rem; line-height: 1.2; } -.themeOfTheMonthSubtitle { - margin: 0.25rem 0 0; - font-size: 0.95rem; - font-weight: 500; - letter-spacing: 0.01em; - text-transform: uppercase; - color: color-mix(in srgb, var(--text-primary) 65%, transparent); +.themeOfTheMonthCardDescription { + display: -webkit-box; + margin: 8px 0 14px; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + font-size: 0.92rem; + line-height: 1.45; + color: color-mix(in srgb, var(--text-primary) 78%, transparent); } -.themeOfTheMonthFooter { +.themeOfTheMonthCardActions { display: flex; - justify-content: center; - padding: 1rem 0; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; } -.themeOfTheMonthViewButton { +.themeOfTheMonthCardPrimary, +.themeOfTheMonthCardSecondary { appearance: none; border: none; cursor: pointer; - padding: 0.65rem 1.25rem; border-radius: 9999px; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.01em; + 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; +} +.themeOfTheMonthCardPrimary { background: var(--better-pri, #6366f1); color: white; - transition: transform 0.15s ease, filter 0.15s ease; } -.themeOfTheMonthViewButton:hover { - filter: brightness(1.1); - transform: scale(1.03); +.themeOfTheMonthCardSecondary { + background: color-mix(in srgb, var(--text-primary) 10%, transparent); + color: var(--text-primary); } -.themeOfTheMonthViewButton:active { - transform: scale(0.98); +.themeOfTheMonthCardPrimary:hover, +.themeOfTheMonthCardSecondary:hover { + filter: brightness(1.08); + transform: translateY(-1px); +} +.themeOfTheMonthCardPrimary:active, +.themeOfTheMonthCardSecondary:active { + transform: translateY(0); +} +@keyframes themeOfTheMonthCardIn { + from { + opacity: 0; + transform: translateY(18px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} +@keyframes themeOfTheMonthCardOut { + to { + opacity: 0; + transform: translateY(12px) scale(0.98); + } +} +@media (max-width: 900px) { + .themeOfTheMonthCard { + z-index: 2147483645; + } } .bsplus-theme-highlight { @@ -4428,38 +4519,63 @@ h2.home-subtitle { .bsplus-toast { position: fixed; - bottom: 24px; - right: 24px; + right: max(18px, env(safe-area-inset-right)); + bottom: max(18px, env(safe-area-inset-bottom)); z-index: 10000; - display: flex; - align-items: flex-start; - gap: 12px; - max-width: 380px; - padding: 16px 18px; - border-radius: 12px; - background: var(--background-secondary, #fff); + width: min(360px, calc(100vw - 36px)); + padding: 14px 16px 16px; + border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent); + border-radius: 20px; + background: var(--background-primary, #fff); color: var(--text-primary, #1a1a1a); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18); + box-shadow: 0 22px 70px rgba(0, 0, 0, 0.35); font-size: 0.9rem; - line-height: 1.5; + line-height: 1.45; } -.bsplus-toast-content p { - margin: 6px 0 0; - opacity: 0.8; - font-size: 0.85rem; +.bsplus-toast-eyebrow { + margin: 0 0 6px !important; + font-size: 0.72rem !important; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: color-mix(in srgb, #ea580c 82%, var(--text-primary) 18%); + opacity: 1 !important; +} +.dark .bsplus-toast-eyebrow { + color: color-mix(in srgb, #fb923c 82%, var(--text-primary) 18%); +} +.bsplus-toast-content strong { + display: block; + padding-right: 34px; + font-size: 1.2rem; + line-height: 1.2; +} +.bsplus-toast-content p:not(.bsplus-toast-eyebrow) { + display: -webkit-box; + margin: 8px 0 0; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + color: color-mix(in srgb, var(--text-primary) 78%, transparent); + font-size: 0.92rem; + line-height: 1.45; } .bsplus-toast-close { - flex-shrink: 0; - background: none; - border: none; - color: var(--text-primary, #1a1a1a); - font-size: 1.3rem; + 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; cursor: pointer; - padding: 0 2px; + font-size: 1.35rem; line-height: 1; - opacity: 0.5; - transition: opacity 0.15s; + transition: filter 0.15s ease; } .bsplus-toast-close:hover { - opacity: 1; + filter: brightness(1.08); } diff --git a/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts b/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts index 1cd72939..126f9e51 100644 --- a/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts +++ b/src/seqta/utils/Openers/OpenEngageParentsAnnouncement.ts @@ -14,13 +14,14 @@ export function showEngageParentsToast() { settingsState.engageParentsAnnouncementShown = true; const toast = document.createElement("div"); - toast.className = "bsplus-toast"; + toast.className = "bsplus-toast engageParentsToast"; toast.innerHTML = /* html */ ` +
+

SEQTA Engage support

BetterSEQTA+ now supports SEQTA Engage

Buy your mum a BetterSEQTA Plus! Parents now get themes, a cleaner home page, and all the Plus polish on SEQTA Engage.

- `; toast.style.opacity = "0"; diff --git a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts index e8f1f0fa..aaf6348a 100644 --- a/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts +++ b/src/seqta/utils/Openers/OpenThemeOfTheMonthPopup.ts @@ -1,7 +1,7 @@ import browser from "webextension-polyfill"; import stringToHTML from "../stringToHTML"; import { settingsState } from "../listeners/SettingsState"; -import { closePopup, openPopup } from "./PopupManager"; +import { closePopup } from "./PopupManager"; import { getApiBase } from "../DevApiBase"; import { openThemeStoreWithHighlight } from "../openThemeStoreWithHighlight"; import { cloudAuth } from "../CloudAuth"; @@ -47,7 +47,7 @@ export async function fetchThemeOfTheMonth(): Promise void, + markSeen = true, +) { + if (card.classList.contains("themeOfTheMonthCardClosing")) return; - const img = document.createElement("img"); - img.src = imageUrl; - img.alt = alt; - img.classList.add("whatsnewImg"); - container.appendChild(img); + if (markSeen) { + const entryId = card.dataset.entryId; + if (entryId) settingsState.themeOfTheMonthLastSeenId = entryId; + } - return container; + card.classList.add("themeOfTheMonthCardClosing"); + window.setTimeout(() => { + card.remove(); + onDismissed?.(); + }, 180); } /** - * Renders the Theme of the Month announcement popup. + * Renders the Theme of the Month announcement card. */ export async function OpenThemeOfTheMonthPopup( entry: ThemeOfTheMonthEntry, onDismissed?: () => void, ) { - if (document.getElementById("whatsnewbk")) { - onDismissed?.(); - return; - } + document.getElementById("theme-of-the-month-card")?.remove(); const monthLabel = formatMonthLabel(entry.month); - - const header = stringToHTML( - /* html */ ` -
-

${escapeHTML(entry.title)}

-

Theme of the Month · ${escapeHTML(monthLabel)}

-
`, - ).firstChild as HTMLElement; - const heroUrl = await resolvePopupHeroImageUrl(entry); - const imageContainer = heroUrl ? createHeroImageContainer(heroUrl, entry.title) : null; + const description = escapeHTML(entry.description).replace(/\n/g, " "); + const linkedThemeId = entry.theme_id ?? entry.theme?.id; - const descriptionHTML = escapeHTML(entry.description).replace(/\n/g, "
"); - const text = stringToHTML(/* html */ ` -
-

${descriptionHTML}

-
+ const card = stringToHTML(/* html */ ` + `).firstChild as HTMLElement; - let footer: HTMLElement | null = null; - const linkedThemeId = entry.theme_id ?? entry.theme?.id; - const linkedThemeName = entry.theme?.name; - if (linkedThemeId && linkedThemeName) { - footer = document.createElement("div"); - footer.classList.add("whatsnewFooter", "themeOfTheMonthFooter"); + card.dataset.entryId = entry.id; + const autoCloseTimeout = window.setTimeout(() => { + closeThemeOfTheMonthCard(card, onDismissed); + }, 12000); - const viewBtn = document.createElement("button"); - viewBtn.type = "button"; - viewBtn.classList.add("themeOfTheMonthViewButton"); - viewBtn.textContent = `View "${linkedThemeName}" in the Theme Store`; - viewBtn.addEventListener("click", () => { - void closePopup(); - openThemeStoreWithHighlight(linkedThemeId); - }); + const dismiss = (markSeen = true) => { + window.clearTimeout(autoCloseTimeout); + closeThemeOfTheMonthCard(card, onDismissed, markSeen); + }; - footer.appendChild(viewBtn); - } + card.addEventListener("mouseenter", () => window.clearTimeout(autoCloseTimeout), { once: true }); - settingsState.themeOfTheMonthLastSeenId = entry.id; - - const content: (Node | null)[] = []; - if (imageContainer) content.push(imageContainer); - content.push(text); - if (footer) content.push(footer); - - openPopup({ - header, - content, - afterClose: onDismissed, + card.querySelector(".themeOfTheMonthCardClose")?.addEventListener("click", () => { + dismiss(); }); + + card.querySelector(".themeOfTheMonthCardPrimary")?.addEventListener("click", () => { + dismiss(); + openThemeStoreWithHighlight(linkedThemeId!); + }); + + card.querySelector(".themeOfTheMonthCardSecondary")?.addEventListener("click", () => { + settingsState.themeOfTheMonthDisabled = true; + dismiss(); + }); + + document.body.appendChild(card); } /** diff --git a/src/types/storage.ts b/src/types/storage.ts index 29224a07..d859d5f5 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -38,6 +38,8 @@ export interface SettingsState { bsCloudAutoSyncAnnouncementShown?: boolean; /** ID of the last Theme of the Month entry shown to the user (shows once per new entry). */ themeOfTheMonthLastSeenId?: string; + /** Permanently disables Theme of the Month startup prompts. */ + themeOfTheMonthDisabled?: boolean; timeFormat?: string; animations: boolean; defaultPage: string;