mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: refine startup announcement cards
This commit is contained in:
@@ -495,6 +495,7 @@ function getDefaultValues(): SettingsState {
|
||||
adaptiveThemeColour: false,
|
||||
adaptiveThemeGradient: false,
|
||||
adaptiveThemeColourTransition: true,
|
||||
themeOfTheMonthDisabled: false,
|
||||
autoCloudSettingsSync: true,
|
||||
};
|
||||
}
|
||||
|
||||
+165
-49
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 */ `
|
||||
<button class="bsplus-toast-close" aria-label="Dismiss">×</button>
|
||||
<div class="bsplus-toast-content">
|
||||
<p class="bsplus-toast-eyebrow">SEQTA Engage support</p>
|
||||
<strong>BetterSEQTA+ now supports <span class="seqtaEngageAccent">SEQTA Engage</span></strong>
|
||||
<p>Buy your mum a BetterSEQTA Plus! Parents now get themes, a cleaner home page, and all the Plus polish on SEQTA Engage.</p>
|
||||
</div>
|
||||
<button class="bsplus-toast-close" aria-label="Dismiss">×</button>
|
||||
`;
|
||||
|
||||
toast.style.opacity = "0";
|
||||
|
||||
@@ -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<ThemeOfTheMonthEntry | nul
|
||||
|
||||
/** True when we have a new monthly entry the user hasn't dismissed yet. */
|
||||
export function shouldShowThemeOfTheMonth(entry: ThemeOfTheMonthEntry | null): boolean {
|
||||
if (!entry) return false;
|
||||
if (!entry || settingsState.themeOfTheMonthDisabled) return false;
|
||||
return settingsState.themeOfTheMonthLastSeenId !== entry.id;
|
||||
}
|
||||
|
||||
@@ -108,82 +108,90 @@ async function resolvePopupHeroImageUrl(entry: ThemeOfTheMonthEntry): Promise<st
|
||||
return fallback || null;
|
||||
}
|
||||
|
||||
function createHeroImageContainer(imageUrl: string, alt: string): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("whatsnewImgContainer");
|
||||
function closeThemeOfTheMonthCard(
|
||||
card: HTMLElement,
|
||||
onDismissed?: () => 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 */ `
|
||||
<div class="whatsnewHeader themeOfTheMonthHeader">
|
||||
<h1>${escapeHTML(entry.title)}</h1>
|
||||
<p class="themeOfTheMonthSubtitle">Theme of the Month · ${escapeHTML(monthLabel)}</p>
|
||||
</div>`,
|
||||
).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, "<br />");
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer themeOfTheMonthDescription" style="height: 50%; overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
|
||||
<p>${descriptionHTML}</p>
|
||||
</div>
|
||||
const card = stringToHTML(/* html */ `
|
||||
<aside id="theme-of-the-month-card" class="themeOfTheMonthCard" role="dialog" aria-label="Theme of the Month">
|
||||
<button type="button" class="themeOfTheMonthCardClose" aria-label="Close Theme of the Month">×</button>
|
||||
${
|
||||
heroUrl
|
||||
? `<img class="themeOfTheMonthCardImage" src="${escapeHTML(heroUrl)}" alt="${escapeHTML(entry.title)}" />`
|
||||
: ""
|
||||
}
|
||||
<div class="themeOfTheMonthCardBody">
|
||||
<p class="themeOfTheMonthCardEyebrow">Theme of the Month · ${escapeHTML(monthLabel)}</p>
|
||||
<h2>${escapeHTML(entry.title)}</h2>
|
||||
<p class="themeOfTheMonthCardDescription">${description}</p>
|
||||
<div class="themeOfTheMonthCardActions">
|
||||
${
|
||||
linkedThemeId
|
||||
? `<button type="button" class="themeOfTheMonthCardPrimary">Open Store</button>`
|
||||
: ""
|
||||
}
|
||||
<button type="button" class="themeOfTheMonthCardSecondary">Don't show again</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`).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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user