feat: new settings popup

This commit is contained in:
2026-07-24 10:08:03 +09:30
parent b5347e2732
commit 928f05ec8e
12 changed files with 662 additions and 354 deletions
+8
View File
@@ -274,6 +274,14 @@ select option {
justify-content: center; justify-content: center;
width: 35px !important; width: 35px !important;
} }
/* Engage toolbar: keep settings/theme buttons above React chrome */
.connectedNotificationsWrapper .addedButton,
.connectedNotificationsWrapper #AddedSettings,
.connectedNotificationsWrapper #LightDarkModeButton {
z-index: 50 !important;
pointer-events: auto !important;
}
[style="background-color: rgb(255, 255, 255);"] { [style="background-color: rgb(255, 255, 255);"] {
color: black; color: black;
} }
+13 -13
View File
@@ -14,11 +14,12 @@
.outside-container { .outside-container {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
position: absolute; position: fixed;
right: 10px; inset: 0;
top: 80px; width: 100%;
height: 590px; height: 100%;
z-index: 100; /* Above Engage React chrome / MUI layers; below rare fullscreen media overlays */
z-index: 200000;
transition-duration: 100ms; transition-duration: 100ms;
} }
@@ -31,12 +32,11 @@
} }
#ExtensionPopup { #ExtensionPopup {
border-radius: 1rem; border-radius: 0;
filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.6)); filter: none;
transform-origin: 70% 0; /* Avoid transform on the host — it makes shadow `position: fixed` children
will-change: opacity, transform; size to the host instead of the viewport on Engage stacking contexts. */
transform: translateZ(0); // promotes GPU rendering transform-origin: center center;
transition: will-change: opacity;
opacity 0.05s, transition: opacity 0.05s;
transform 0.05s;
} }
@@ -128,7 +128,8 @@
const openStorePage = async () => { const openStorePage = async () => {
const { OpenStorePage } = await import('@/seqta/ui/renderStore') const { OpenStorePage } = await import('@/seqta/ui/renderStore')
OpenStorePage() closeExtensionPopup()
await OpenStorePage()
} }
const openThemeCreator = async (themeId?: string) => { const openThemeCreator = async (themeId?: string) => {
+265 -57
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import TabbedContainer from "../components/TabbedContainer.svelte";
import Settings from "./settings/general.svelte"; import Settings from "./settings/general.svelte";
import Shortcuts from "./settings/shortcuts.svelte"; import Shortcuts from "./settings/shortcuts.svelte";
import Theme from "./settings/theme.svelte"; import Theme from "./settings/theme.svelte";
@@ -25,20 +24,64 @@
isGhReleaseUpdateCheckEnabled, isGhReleaseUpdateCheckEnabled,
type GhReleaseUpdateInfo, type GhReleaseUpdateInfo,
} from "@/utils/githubReleaseUpdate"; } from "@/utils/githubReleaseUpdate";
import { getAllPluginSettings } from "@/plugins";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
type PageId = "settings" | "shortcuts" | "themes";
type NavItem = {
id: string;
label: string;
};
let devModeSequence = ""; let devModeSequence = "";
let settingsActiveTab = $state(0); let activePage = $state<PageId>("settings");
let activeSection = $state("general");
let showDisclaimerModal = $state(false); let showDisclaimerModal = $state(false);
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null); let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
let disclaimerTitle = $state("Confirm"); let disclaimerTitle = $state("Confirm");
let disclaimerMessage = $state(""); let disclaimerMessage = $state("");
const ghReleaseUpdateEnabled = isGhReleaseUpdateCheckEnabled(); const ghReleaseUpdateEnabled = isGhReleaseUpdateCheckEnabled();
const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel(); const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel();
let ghReleaseUpdate = $state<GhReleaseUpdateInfo | null>(null); let ghReleaseUpdate = $state<GhReleaseUpdateInfo | null>(null);
const pages: { id: PageId; title: string }[] = [
{ id: "settings", title: "Settings" },
{ id: "shortcuts", title: "Shortcuts" },
{ id: "themes", title: "Themes" },
];
const pluginNavItems = getAllPluginSettings()
.filter((plugin) => !(isSeqtaEngageExperience() && plugin.pluginId === "global-search"))
.filter(
(plugin) =>
(plugin as { disableToggle?: boolean }).disableToggle ||
Object.keys(plugin.settings ?? {}).length > 0,
)
.map((plugin) => ({
id: `plugin:${plugin.pluginId}`,
label: plugin.name,
}));
const userNav: NavItem[] = [
{ id: "account", label: "My Account" },
{ id: "general", label: "General" },
{ id: "appearance", label: "Appearance" },
{ id: "home", label: "Home" },
];
const appNav: NavItem[] = [...pluginNavItems, { id: "advanced", label: "Advanced" }];
const sectionTitle = $derived.by(() => {
if (activePage === "shortcuts") return "Shortcuts";
if (activePage === "themes") return "Themes";
const all = [...userNav, ...appNav];
return all.find((item) => item.id === activeSection)?.label ?? "Settings";
});
const openGhRelease = () => { const openGhRelease = () => {
const url = ghReleaseUpdate?.url const url =
?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases"; ghReleaseUpdate?.url ?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
if (ghReleaseUpdate?.available) { if (ghReleaseUpdate?.available) {
dismissNightlyUpdate(); dismissNightlyUpdate();
} }
@@ -118,6 +161,21 @@
showCloudPanel = false; showCloudPanel = false;
}; };
const handleClose = () => {
if (!standalone) {
closeExtensionPopup();
}
};
const selectPage = (page: PageId) => {
activePage = page;
};
const selectSection = (id: string) => {
activeSection = id;
activePage = "settings";
};
onMount(() => { onMount(() => {
settingsPopup.addListener(closePopupsOnSettingsClose); settingsPopup.addListener(closePopupsOnSettingsClose);
@@ -130,6 +188,17 @@
ghReleaseUpdate = info; ghReleaseUpdate = info;
}); });
} }
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !standalone) {
closeExtensionPopup();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}); });
onDestroy(() => { onDestroy(() => {
@@ -137,120 +206,259 @@
}); });
</script> </script>
<div {#snippet navButton(item: NavItem)}
class="relative w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode <button
? 'dark' type="button"
: ''} {standalone ? 'h-[600px]' : 'h-full rounded-xl'} overflow-clip" onclick={() => selectSection(item.id)}
> class="w-full px-3 py-2 text-left text-base rounded-lg transition-all duration-200
<div {activePage === 'settings' && activeSection === item.id
class="flex relative flex-col gap-2 h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white" ? 'bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white font-medium'
: 'text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-700/40 hover:text-zinc-900 dark:hover:text-white'}"
> >
{item.label}
</button>
{/snippet}
{#snippet settingsShell()}
<div <div
class="grid shrink-0 place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40" class="flex flex-col h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white {standalone
? ''
: 'rounded-xl shadow-2xl border border-zinc-200/60 dark:border-zinc-700/60'}"
>
<!-- Top bar: logo + page selectors + actions -->
<div
class="flex shrink-0 items-center gap-4 px-5 py-4 border-b border-zinc-200/60 dark:border-zinc-700/50"
> >
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<img <img
src={browser.runtime.getURL( src={browser.runtime.getURL("resources/icons/betterseqta-dark-full.png")}
"resources/icons/betterseqta-dark-full.png", class="h-9 w-auto dark:hidden shrink-0 cursor-pointer"
)} alt="BetterSEQTA+"
class="w-4/5 dark:hidden"
alt="Light logo"
onclick={handleDevModeToggle} onclick={handleDevModeToggle}
/> />
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<img <img
src={browser.runtime.getURL( src={browser.runtime.getURL("resources/icons/betterseqta-light-full.png")}
"resources/icons/betterseqta-light-full.png", class="hidden h-9 w-auto dark:block shrink-0 cursor-pointer"
)} alt="BetterSEQTA+"
class="hidden w-4/5 dark:block"
alt="Dark logo"
onclick={handleDevModeToggle} onclick={handleDevModeToggle}
/> />
<div
class="flex flex-1 items-center justify-center gap-1 p-1.5 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50"
role="tablist"
aria-label="Settings pages"
>
{#each pages as page (page.id)}
<button
type="button"
role="tab"
aria-selected={activePage === page.id}
onclick={() => selectPage(page.id)}
class="flex-1 px-4 py-2 text-base rounded-full transition-all duration-200
{activePage === page.id
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white font-semibold shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200'}"
>
{page.title}
</button>
{/each}
</div>
{#if !standalone} {#if !standalone}
<div class="flex absolute top-1 right-1 gap-1 items-start"> <div class="flex items-center gap-1 shrink-0">
{#if ghReleaseUpdateEnabled} {#if ghReleaseUpdateEnabled}
<div class="flex flex-col items-end gap-0.5 max-w-[9rem] mr-0.5"> <div class="flex flex-col items-end gap-0.5 max-w-[8.5rem] mr-0.5">
{#if ghReleaseUpdate?.available} {#if ghReleaseUpdate?.available}
<button <button
type="button" type="button"
onclick={openGhRelease} onclick={openGhRelease}
class="px-1.5 py-0.5 text-[10px] font-semibold leading-tight text-white rounded-full bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500" class="px-1.5 py-0.5 text-[10px] font-semibold leading-tight text-white rounded-full bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
title="Open GitHub release" title="Open GitHub release"
> >
Update available {ghReleaseUpdate.label} Update — {ghReleaseUpdate.label}
</button> </button>
{/if} {/if}
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400"> <p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
{#if ghReleaseChannelLabel} {#if ghReleaseChannelLabel}
{ghReleaseChannelLabel} — do not upload to extension stores. {ghReleaseChannelLabel} — do not upload to stores.
{:else} {:else}
GitHub release build — do not upload to extension stores. GitHub build — do not upload to stores.
{/if} {/if}
</p> </p>
</div> </div>
{/if} {/if}
<div class="flex gap-1 items-center">
<button <button
type="button"
onclick={openAbout} onclick={openAbout}
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700" class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
aria-label="About"
> >
{"\ueb73"} &#xeb73;
</button> </button>
<button <button
type="button"
onclick={openChangelog} onclick={openChangelog}
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700" class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
aria-label="Changelog"
> >
{"\ue929"} &#xe929;
</button> </button>
<button <button
type="button"
onclick={openPrivacyStatement} onclick={openPrivacyStatement}
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700" class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
aria-label="Privacy Statement" aria-label="Privacy Statement"
> >
{"\uecba"} &#xecba;
</button>
<button
type="button"
onclick={handleClose}
class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
aria-label="Close settings"
>
&#xea0f;
</button> </button>
</div>
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex-1 min-h-0 overflow-hidden"> <!-- Body: left nav + content -->
<TabbedContainer <div class="flex flex-1 min-h-0 overflow-hidden">
bind:activeTab={settingsActiveTab} <nav
tabs={[ class="flex flex-col shrink-0 gap-5 overflow-y-auto no-scrollbar border-r border-zinc-200/60 dark:border-zinc-700/50 bg-zinc-50/80 dark:bg-zinc-900/40 {standalone
{ ? 'w-[140px] px-2 py-3'
title: "Settings", : 'w-[260px] px-4 py-5'}"
Content: Settings, aria-label="Settings categories"
props: { showColourPicker: openColourPicker, showFontPicker: openFontPicker, showDisclaimer, showCloudPanel: openCloudPanel }, >
}, {#if activePage === "settings"}
{ title: "Shortcuts", Content: Shortcuts }, <div class="flex flex-col gap-1">
{ title: "Themes", Content: Theme }, <p
]} class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
/> >
User Settings
</p>
{#each userNav as item (item.id)}
{@render navButton(item)}
{/each}
</div> </div>
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
App Settings
</p>
{#each appNav as item (item.id)}
{@render navButton(item)}
{/each}
</div> </div>
{:else if activePage === "shortcuts"}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Shortcuts
</p>
<button
type="button"
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
>
Shortcuts
</button>
</div>
{:else}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Themes
</p>
<button
type="button"
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
>
Themes
</button>
</div>
{/if}
</nav>
{#if showColourPicker && ColourPickerComponent} <div class="flex flex-col flex-1 min-w-0 min-h-0">
<div class="shrink-0 px-6 pt-5 pb-3">
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">
{sectionTitle}
</h1>
</div>
<div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar">
{#if activePage === "settings"}
<Settings
showColourPicker={openColourPicker}
showFontPicker={openFontPicker}
{showDisclaimer}
showCloudPanel={openCloudPanel}
{activeSection}
/>
{:else if activePage === "shortcuts"}
<Shortcuts />
{:else}
<Theme />
{/if}
</div>
</div>
</div>
</div>
{/snippet}
{#if standalone}
<div
class="relative w-[384px] h-[600px] no-scrollbar shadow-2xl overflow-clip {$settingsState.DarkMode
? 'dark'
: ''}"
>
{@render settingsShell()}
</div>
{:else}
<div
class="absolute inset-0 z-50 flex items-center justify-center p-4 sm:p-6 {$settingsState.DarkMode
? 'dark'
: ''}"
role="dialog"
aria-modal="true"
aria-label="BetterSEQTA+ settings"
>
<button
type="button"
class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-colors duration-200"
aria-label="Close settings"
onclick={handleClose}
></button>
<div
class="relative z-10 w-[min(1180px,96vw)] h-[min(860px,92vh)] no-scrollbar overflow-clip"
>
{@render settingsShell()}
</div>
</div>
{/if}
{#if showColourPicker && ColourPickerComponent}
<ColourPickerComponent <ColourPickerComponent
hidePicker={() => { hidePicker={() => {
showColourPicker = false; showColourPicker = false;
}} }}
/> />
{/if} {/if}
{#if showCloudPanel} {#if showCloudPanel}
<CloudPanel <CloudPanel
hidePanel={() => { hidePanel={() => {
showCloudPanel = false; showCloudPanel = false;
}} }}
/> />
{/if} {/if}
</div>
{#if showFontPicker} {#if showFontPicker}
<FontPickerModal <FontPickerModal
+180 -139
View File
@@ -47,6 +47,8 @@
import { getAllPluginSettings } from "@/plugins" import { getAllPluginSettings } from "@/plugins"
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage" import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"
const isEngage = isSeqtaEngageExperience();
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types" import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
// Union type representing all possible settings // Union type representing all possible settings
@@ -133,13 +135,20 @@
void loadPluginSettings(); void loadPluginSettings();
}); });
const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel } = $props<{ const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel, activeSection = "general" } = $props<{
showColourPicker: () => void; showColourPicker: () => void;
showFontPicker: () => void; showFontPicker: () => void;
showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void; showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void;
showCloudPanel: () => void; showCloudPanel: () => void;
activeSection?: string;
}>(); }>();
const activePluginId = $derived(
activeSection.startsWith("plugin:")
? activeSection.slice("plugin:".length)
: null,
);
async function exportCloudSettingsJsonToFile() { async function exportCloudSettingsJsonToFile() {
const payload = await getSnapshotForUpload(); const payload = await getSnapshotForUpload();
const blob = new Blob([JSON.stringify(payload, null, 2)], { const blob = new Blob([JSON.stringify(payload, null, 2)], {
@@ -155,26 +164,51 @@
</script> </script>
{#snippet Setting({ title, description, Component, props }: SettingsList) } {#snippet Setting({ title, description, Component, props }: SettingsList) }
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-5">
<h2 class="text-sm font-bold">{title}</h2> <h2 class="text-base font-bold">{title}</h2>
<p class="text-xs">{description}</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">{description}</p>
</div> </div>
<div> <div class="shrink-0">
<Component {...props} /> <Component {...props} />
</div> </div>
</div> </div>
{/snippet} {/snippet}
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700"> <div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
{#each [ {#if activeSection === "account"}
{ {@render Setting({
title: "Connect Mobile App", title: "Connect Mobile App",
description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn", description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn",
id: 0, id: 0,
Component: ConnectMobileApp, Component: ConnectMobileApp,
props: {} props: {}
}, })}
<div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">BetterSEQTA Cloud</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Account & sync</p>
</div>
<div>
<CloudHeader alwaysShowUserName onClick={showCloudPanel} />
</div>
</div>
{#if cloudState.isLoggedIn}
<div class="px-3 pb-3">
<CloudSettingsSync showDisclaimer={(onConfirm, onCancel) => showDisclaimer(onConfirm, onCancel, "Restore from cloud?", "This will replace your local settings with the cloud backup. Continue?")} />
</div>
{/if}
</div>
</div>
{/if}
{#if activeSection === "general"}
{#each [
...(!isEngage
? [
{ {
title: "Edit Sidebar Layout", title: "Edit Sidebar Layout",
description: "Reorder pages on the sidebar", description: "Reorder pages on the sidebar",
@@ -185,25 +219,6 @@
text: "Edit" text: "Edit"
} }
}, },
{
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn",
id: 4,
Component: PickerSwatch,
props: {
onClick: showColourPicker
}
},
{
title: "Interface Font",
description: "Choose the typeface used across SEQTA Learn",
id: 16,
Component: Button,
props: {
onClick: showFontPicker,
text: "Change"
}
},
{ {
title: "Icon Only Sidebar", title: "Icon Only Sidebar",
description: "Show only icons in the sidebar for a compact layout", description: "Show only icons in the sidebar for a compact layout",
@@ -214,6 +229,8 @@
onChange: (isOn: boolean) => settingsState.iconOnlySidebar = isOn onChange: (isOn: boolean) => settingsState.iconOnlySidebar = isOn
} }
}, },
]
: []),
{ {
title: "Animations", title: "Animations",
description: "Enable animations on certain pages", description: "Enable animations on certain pages",
@@ -252,7 +269,16 @@
props: { props: {
value: $settingsState.defaultPage ?? "home", value: $settingsState.defaultPage ?? "home",
onChange: (value: string) => (settingsState.defaultPage = value), onChange: (value: string) => (settingsState.defaultPage = value),
options: [ options: isEngage
? [
{ value: "home", label: "Home" },
{ value: "dashboard", label: "Dashboard" },
{ value: "timetable", label: "Timetable" },
{ value: "messages", label: "Messages" },
{ value: "documents", label: "Documents" },
{ value: "reports", label: "Reports" },
]
: [
{ value: "home", label: "Home" }, { value: "home", label: "Home" },
{ value: "dashboard", label: "Dashboard" }, { value: "dashboard", label: "Dashboard" },
{ value: "timetable", label: "Timetable" }, { value: "timetable", label: "Timetable" },
@@ -263,6 +289,8 @@
], ],
}, },
}, },
...(!isEngage
? [
{ {
title: "News Feed Source", title: "News Feed Source",
description: "Choose the sources for your news feed", description: "Choose the sources for your news feed",
@@ -281,27 +309,99 @@
{ value: "canada", label: "Canada" }, { value: "canada", label: "Canada" },
{ value: "singapore", label: "Singapore" }, { value: "singapore", label: "Singapore" },
{ value: "japan", label: "Japan" }, { value: "japan", label: "Japan" },
{ value: "netherlands", label: "Netherlands" } { value: "netherlands", label: "Netherlands" },
],
},
},
] ]
: []),
] as option (option.id)}
{@render Setting(option)}
{/each}
{/if}
{#if activeSection === "appearance"}
{#each [
{
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn",
id: 4,
Component: PickerSwatch,
props: {
onClick: showColourPicker
} }
},
{
title: "Interface Font",
description: "Choose the typeface used across SEQTA Learn",
id: 16,
Component: Button,
props: {
onClick: showFontPicker,
text: "Change"
} }
] as option} },
] as option (option.id)}
{@render Setting(option)} {@render Setting(option)}
{/each} {/each}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Home Page Assessments</h2> <h2 class="text-base font-bold">Adaptive Theme Colour</h2>
<p class="text-xs">Limit upcoming assessments shown on the home page by subject</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeColour ?? false}
onChange={(isOn: boolean) => settingsState.adaptiveThemeColour = isOn}
/>
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50"> {#if $settingsState.adaptiveThemeColour}
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Include Past Assessments</h2> <h2 class="text-base font-bold">Soft Gradient</h2>
<p class="text-xs">Show past-due assessments from the upcoming list, matching the Assessments page</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Use a soft gradient instead of a solid colour when viewing a class</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeGradient ?? false}
onChange={(isOn: boolean) => settingsState.adaptiveThemeGradient = isOn}
/>
</div>
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Smooth colour transition</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Ease between class/subject colours when navigating instead of switching instantly</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeColourTransition ?? true}
onChange={(isOn: boolean) => settingsState.adaptiveThemeColourTransition = isOn}
/>
</div>
</div>
{/if}
</div>
</div>
{/if}
{#if activeSection === "home"}
<div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Home Page Assessments</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Limit upcoming assessments shown on the home page by subject</p>
</div>
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Include Past Assessments</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show past-due assessments from the upcoming list, matching the Assessments page</p>
</div> </div>
<div> <div>
<Switch <Switch
@@ -310,10 +410,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50"> <div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Maximum Subjects</h2> <h2 class="text-base font-bold">Maximum Subjects</h2>
<p class="text-xs">Number of subjects to include, ordered by soonest due date</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Number of subjects to include, ordered by soonest due date</p>
</div> </div>
<Select <Select
value={String($settingsState.homeUpcomingSubjectsMax ?? 5)} value={String($settingsState.homeUpcomingSubjectsMax ?? 5)}
@@ -328,10 +428,10 @@
]} ]}
/> />
</div> </div>
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50"> <div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Maximum Assessments per Subject</h2> <h2 class="text-base font-bold">Maximum Assessments per Subject</h2>
<p class="text-xs">Assessments shown for each included subject</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Assessments shown for each included subject</p>
</div> </div>
<Select <Select
value={String($settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0)} value={String($settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0)}
@@ -348,58 +448,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">Adaptive Theme Colour</h2>
<p class="text-xs">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeColour ?? false}
onChange={(isOn: boolean) => settingsState.adaptiveThemeColour = isOn}
/>
</div>
</div>
{#if $settingsState.adaptiveThemeColour}
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-sm font-bold">Soft Gradient</h2>
<p class="text-xs">Use a soft gradient instead of a solid colour when viewing a class</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeGradient ?? false}
onChange={(isOn: boolean) => settingsState.adaptiveThemeGradient = isOn}
/>
</div>
</div>
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-sm font-bold">Smooth colour transition</h2>
<p class="text-xs">Ease between class/subject colours when navigating instead of switching instantly</p>
</div>
<div>
<Switch
state={$settingsState.adaptiveThemeColourTransition ?? true}
onChange={(isOn: boolean) => settingsState.adaptiveThemeColourTransition = isOn}
/>
</div>
</div>
{/if} {/if}
</div>
</div>
{#each pluginSettings as plugin} {#each pluginSettings as plugin (plugin.pluginId)}
{#if activePluginId === plugin.pluginId}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}">
<!-- Always show enable toggle if disableToggle is true --> <!-- Always show enable toggle if disableToggle is true -->
{#if (plugin as any).disableToggle} {#if (plugin as any).disableToggle}
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="flex gap-2 items-center text-sm font-bold"> <h2 class="flex gap-2 items-center text-base font-bold">
Enable {plugin.name} Enable {plugin.name}
{#if plugin.beta} {#if plugin.beta}
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30"> <span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30">
@@ -407,7 +466,7 @@
</span> </span>
{/if} {/if}
</h2> </h2>
<p class="text-xs">{plugin.description}</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">{plugin.description}</p>
</div> </div>
<div> <div>
<Switch <Switch
@@ -432,13 +491,13 @@
{/if} {/if}
{#if !((plugin as any).disableToggle) || (pluginSettingsValues[plugin.pluginId]?.enabled ?? true)} {#if !((plugin as any).disableToggle) || (pluginSettingsValues[plugin.pluginId]?.enabled ?? true)}
{#each Object.entries(plugin.settings) as [key, setting]} {#each Object.entries(plugin.settings) as [key, setting] (key)}
<!-- Skip the 'enabled' setting and hide cloud-only settings when not signed in --> <!-- Skip the 'enabled' setting and hide cloud-only settings when not signed in -->
{#if key !== 'enabled' && !(key === 'useCloudPfp' && !cloudState.isLoggedIn)} {#if key !== 'enabled' && !(key === 'useCloudPfp' && !cloudState.isLoggedIn)}
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">{setting.title || key}</h2> <h2 class="text-base font-bold">{setting.title || key}</h2>
<p class="text-xs">{setting.description || ''}</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">{setting.description || ''}</p>
</div> </div>
<div> <div>
{#if setting.type === 'boolean'} {#if setting.type === 'boolean'}
@@ -507,29 +566,10 @@
})} })}
{/if} {/if}
</div> </div>
{/if}
{/each} {/each}
<div class="border-none"> {#if activeSection === "advanced"}
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">BetterSEQTA Cloud</h2>
<p class="text-xs">Account & sync</p>
</div>
<div>
<CloudHeader alwaysShowUserName onClick={showCloudPanel} />
</div>
</div>
{#if cloudState.isLoggedIn}
<div class="px-3 pb-3">
<CloudSettingsSync showDisclaimer={(onConfirm, onCancel) => showDisclaimer(onConfirm, onCancel, "Restore from cloud?", "This will replace your local settings with the cloud backup. Continue?")} />
</div>
{/if}
</div>
</div>
<div class="p-1 border-none"></div>
{@render Setting({ {@render Setting({
title: "BetterSEQTA+", title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features", description: "Enables BetterSEQTA+ features",
@@ -543,19 +583,19 @@
{#if $settingsState.devMode} {#if $settingsState.devMode}
<div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Developer Mode</h2> <h2 class="text-base font-bold">Developer Mode</h2>
<p class="text-xs">Enables developer mode, allowing you to test new features and changes.</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Enables developer mode, allowing you to test new features and changes.</p>
</div> </div>
<div> <div>
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} /> <Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Verbose logging</h2> <h2 class="text-base font-bold">Verbose logging</h2>
<p class="text-xs">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
</div> </div>
<div> <div>
<Switch <Switch
@@ -564,10 +604,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Sensitive Hider</h2> <h2 class="text-base font-bold">Sensitive Hider</h2>
<p class="text-xs">Replace sensitive content with mock data</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Replace sensitive content with mock data</p>
</div> </div>
<div> <div>
<Switch <Switch
@@ -576,10 +616,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Mock Notices</h2> <h2 class="text-base font-bold">Mock Notices</h2>
<p class="text-xs">Use fake notice data on homepage instead of real data</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Use fake notice data on homepage instead of real data</p>
</div> </div>
<div> <div>
<Switch <Switch
@@ -588,10 +628,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Show Privacy Notification</h2> <h2 class="text-base font-bold">Show Privacy Notification</h2>
<p class="text-xs">Show the privacy notification popup on next page load</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Show the privacy notification popup on next page load</p>
</div> </div>
<div> <div>
<Button <Button
@@ -607,10 +647,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Show Theme of the Month</h2> <h2 class="text-base font-bold">Show Theme of the Month</h2>
<p class="text-xs">Fetch and show the current month's popup now (ignores dismissed state)</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Fetch and show the current month's popup now (ignores dismissed state)</p>
</div> </div>
<div> <div>
<Button <Button
@@ -623,10 +663,10 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Export cloud settings JSON</h2> <h2 class="text-base font-bold">Export cloud settings JSON</h2>
<p class="text-xs">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p>
</div> </div>
<div> <div>
<Button onClick={exportCloudSettingsJsonToFile} text="Export to file" /> <Button onClick={exportCloudSettingsJsonToFile} text="Export to file" />
@@ -635,8 +675,8 @@
<div class="flex flex-col gap-2 px-4 py-3"> <div class="flex flex-col gap-2 px-4 py-3">
<div class="flex justify-between items-start gap-3"> <div class="flex justify-between items-start gap-3">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">API Base URL (session only)</h2> <h2 class="text-base font-bold">API Base URL (session only)</h2>
<p class="text-xs">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p>
{#if devApiBaseActive} {#if devApiBaseActive}
<p class="text-xs mt-1 text-amber-600 dark:text-amber-400"> <p class="text-xs mt-1 text-amber-600 dark:text-amber-400">
Override active: <span class="font-mono">{devApiBaseActive}</span> Override active: <span class="font-mono">{devApiBaseActive}</span>
@@ -659,8 +699,8 @@
</div> </div>
<div class="flex flex-col gap-2 px-4 py-3"> <div class="flex flex-col gap-2 px-4 py-3">
<div> <div>
<h2 class="text-sm font-bold">GitHub latest version override</h2> <h2 class="text-base font-bold">GitHub latest version override</h2>
<p class="text-xs">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p> <p class="text-sm text-zinc-600 dark:text-zinc-300">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
</div> </div>
<input <input
type="text" type="text"
@@ -674,4 +714,5 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
</div> </div>
+9 -4
View File
@@ -832,12 +832,17 @@ export function AppendElementsToDisabledPage() {
margin: 6px; margin: 6px;
} }
.outside-container { .outside-container {
top: 48px !important; position: fixed !important;
inset: 0 !important;
top: 0 !important;
width: 100% !important;
height: 100% !important;
z-index: 200000 !important;
} }
#ExtensionPopup { #ExtensionPopup {
border-radius: 1rem; border-radius: 0;
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6); box-shadow: none;
transform-origin: 70% 0; transform-origin: center center;
} }
`; `;
document.head.append(settingsStyle); document.head.append(settingsStyle);
+42 -8
View File
@@ -440,22 +440,56 @@ async function addEngageUserInfo() {
}); });
} }
async function mountEngageToolbarControls(parent?: Element) {
// React can remount the notifications chrome — recreate missing controls.
if (!document.getElementById("LightDarkModeButton")) {
await addDarkLightToggle(parent);
}
if (!document.getElementById("AddedSettings")) {
await createSettingsButton(parent);
}
setupSettingsButton();
attachNotificationsPanelAnimation();
}
async function setupEngageSettingsButton() { async function setupEngageSettingsButton() {
let mounting = false;
const tryMount = async () => {
if (mounting || document.getElementById("AddedSettings")) return;
mounting = true;
try {
try { try {
const notificationsWrapper = await waitForElm( const notificationsWrapper = await waitForElm(
"#content > div.connectedNotificationsWrapper > div", "#content > div.connectedNotificationsWrapper > div",
true,
100,
40,
); );
const parent = notificationsWrapper.parentElement!; const parent = notificationsWrapper.parentElement!;
await addDarkLightToggle(parent); await mountEngageToolbarControls(parent);
await createSettingsButton(parent);
setupSettingsButton();
attachNotificationsPanelAnimation();
} catch { } catch {
await addDarkLightToggle(); await mountEngageToolbarControls();
await createSettingsButton();
setupSettingsButton();
attachNotificationsPanelAnimation();
} }
} finally {
mounting = false;
}
};
await tryMount();
// Re-inject if Engage's React shell wipes the toolbar controls.
const content = document.getElementById("content");
if (!content || (content as HTMLElement).dataset.bsplusEngageSettingsWatch === "1") {
return;
}
(content as HTMLElement).dataset.bsplusEngageSettingsWatch = "1";
const observer = new MutationObserver(() => {
if (document.getElementById("AddedSettings")) return;
void tryMount();
});
observer.observe(content, { childList: true, subtree: true });
} }
function GetLightDarkModeString() { function GetLightDarkModeString() {
+6 -6
View File
@@ -1,7 +1,7 @@
import debounce from "@/seqta/utils/debounce"; import debounce from "@/seqta/utils/debounce";
/** /**
* Automatically resizes the popup to fit the screen, checks on resize but is debounced to prevent intense utilisation. * Keeps the settings overlay covering the viewport.
*/ */
export class SettingsResizer { export class SettingsResizer {
constructor() { constructor() {
@@ -17,10 +17,10 @@ export class SettingsResizer {
const iframePopup = document.getElementById("ExtensionPopup"); const iframePopup = document.getElementById("ExtensionPopup");
if (!iframePopup) return; if (!iframePopup) return;
const viewportHeight = window.innerHeight; iframePopup.style.inset = "0";
const rawIdeal = viewportHeight - 80 - 15; // room below top chrome iframePopup.style.top = "0";
const idealHeight = Math.min(Math.max(rawIdeal, 280), 600); iframePopup.style.right = "0";
iframePopup.style.width = "100%";
iframePopup.style.height = `${idealHeight}px`; iframePopup.style.height = "100%";
} }
} }
+4 -4
View File
@@ -12,10 +12,10 @@ export async function renderStore() {
import("@/interface/pages/store.svelte"), import("@/interface/pages/store.svelte"),
]); ]);
const container = document.querySelector("#container"); const container =
if (!container) { document.querySelector("#container") ??
throw new Error("Container not found"); document.getElementById("content") ??
} document.body;
document.getElementById("store")?.remove(); document.getElementById("store")?.remove();
@@ -13,12 +13,18 @@ function extensionOutsideClickHandler(extensionPopup: HTMLElement) {
if (!SettingsClicked) return; if (!SettingsClicked) return;
if (!(event.target as HTMLElement).closest("#AddedSettings")) { if (!(event.target as HTMLElement).closest("#AddedSettings")) {
// Clicks inside the shadow tree retarget to the host — keep open.
if (event.target == extensionPopup) return; if (event.target == extensionPopup) return;
changeSettingsClicked(closeExtensionPopup()); changeSettingsClicked(closeExtensionPopup());
} }
}; };
} }
/**
* Mount the settings host on `document.body` so `position: fixed` covers the
* viewport on both SEQTA Learn and SEQTA Engage (Engage often lacks `#container`
* or wraps the app in stacking contexts that clip in-app overlays).
*/
export function addExtensionSettings() { export function addExtensionSettings() {
if (document.getElementById("ExtensionPopup")) return; if (document.getElementById("ExtensionPopup")) return;
@@ -26,15 +32,12 @@ export function addExtensionSettings() {
extensionPopup.classList.add("outside-container", "hide"); extensionPopup.classList.add("outside-container", "hide");
extensionPopup.id = "ExtensionPopup"; extensionPopup.id = "ExtensionPopup";
const extensionContainer = document.body.appendChild(extensionPopup);
document.querySelector("#container") ?? document.getElementById("container");
const mountParent = extensionContainer ?? document.body;
mountParent.appendChild(extensionPopup);
new SettingsResizer(); new SettingsResizer();
const handler = extensionOutsideClickHandler(extensionPopup); const handler = extensionOutsideClickHandler(extensionPopup);
(extensionContainer ?? document.body).addEventListener("click", handler, false); document.body.addEventListener("click", handler, false);
} }
async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> { async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
@@ -14,7 +14,6 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
animate(1, 0, { animate(1, 0, {
onUpdate: (progress) => { onUpdate: (progress) => {
extensionPopup.style.opacity = Math.max(0, progress).toString(); extensionPopup.style.opacity = Math.max(0, progress).toString();
extensionPopup.style.transform = `scale(${Math.max(0, progress)})`;
}, },
type: "spring", type: "spring",
stiffness: 520, stiffness: 520,
@@ -22,7 +21,6 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
}); });
} else { } else {
extensionPopup.style.opacity = "0"; extensionPopup.style.opacity = "0";
extensionPopup.style.transform = "scale(0)";
} }
settingsPopup.triggerClose(); settingsPopup.triggerClose();
+17 -7
View File
@@ -10,22 +10,34 @@ import { delay } from "./delay";
export function setupSettingsButton() { export function setupSettingsButton() {
const AddedSettings = document.getElementById("AddedSettings"); const AddedSettings = document.getElementById("AddedSettings");
const extensionPopup = document.getElementById("ExtensionPopup"); if (!AddedSettings) return;
if (!AddedSettings || !extensionPopup) return;
// Avoid stacking duplicate listeners if Engage remounts the toolbar.
if (AddedSettings.dataset.bsplusSettingsBound === "1") return;
AddedSettings.dataset.bsplusSettingsBound = "1";
AddedSettings.addEventListener("click", async () => { AddedSettings.addEventListener("click", async () => {
// Re-query each click — Engage SPA navigations can recreate the host.
let extensionPopup = document.getElementById("ExtensionPopup");
if (!extensionPopup) {
const { addExtensionSettings } = await import("./Adders/AddExtensionSettings");
addExtensionSettings();
extensionPopup = document.getElementById("ExtensionPopup");
}
if (!extensionPopup) return;
if (SettingsClicked) { if (SettingsClicked) {
closeExtensionPopup(extensionPopup as HTMLElement); closeExtensionPopup(extensionPopup);
} else { } else {
await renderSettingsIfNeeded(); await renderSettingsIfNeeded();
await delay(30); await delay(30);
extensionPopup.style.transform = "none";
if (settingsState.animations) { if (settingsState.animations) {
animate(0, 1, { animate(0, 1, {
onUpdate: (progress) => { onUpdate: (progress) => {
extensionPopup.style.opacity = progress.toString(); extensionPopup.style.opacity = progress.toString();
extensionPopup.style.transform = `scale(${progress})`;
}, },
type: "spring", type: "spring",
stiffness: 280, stiffness: 280,
@@ -33,9 +45,7 @@ export function setupSettingsButton() {
}); });
} else { } else {
extensionPopup.style.opacity = "1"; extensionPopup.style.opacity = "1";
extensionPopup.style.transform = "scale(1)"; extensionPopup.style.transition = "opacity 0s linear";
extensionPopup.style.transition =
"opacity 0s linear, transform 0s linear";
} }
extensionPopup.classList.remove("hide"); extensionPopup.classList.remove("hide");
changeSettingsClicked(true); changeSettingsClicked(true);