feat: refine settings, stores, and message folders

This commit is contained in:
SethBurkart123
2026-08-04 11:05:51 +10:00
parent defaf76144
commit ddbccecda2
33 changed files with 922 additions and 705 deletions
+2 -38
View File
@@ -162,40 +162,6 @@ select option {
}
}
#store {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9998;
animation: fadeIn 500ms forwards;
animation-delay: 200ms;
opacity: 0;
&.hide {
animation: fadeOut 500ms forwards;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dark .resizeBar {
background-color: rgb(28 28 30);
}
@@ -1817,12 +1783,10 @@ html.transparencyEffects
-webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
}
/* Not named `.border` — Tailwinds utility of that name is injected into the
page via content-script CSS in production and would paint a 1px outline. */
.bsplus-rounded {
/* `.border` is also a public hook used by existing themes. */
.home-container .border {
border-radius: 16px;
overflow: clip;
border-width: 0;
}
.shortcut-container h2 {
font-size: 20px;
@@ -0,0 +1,25 @@
<script lang="ts">
let { onclick, label = "Close" } = $props<{
onclick: () => void;
label?: string;
}>();
</script>
<button
type="button"
{onclick}
class="flex h-10 w-10 shrink-0 items-center justify-center text-zinc-500 transition-[color,transform] duration-150 hover:text-zinc-950 active:scale-[0.96] focus:outline-none dark:text-zinc-400 dark:hover:text-white"
aria-label={label}
>
<svg
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</button>
@@ -1,6 +1,7 @@
<script lang="ts">
import Select from "@/interface/components/Select.svelte";
import Slider from "@/interface/components/Slider.svelte";
import Switch from "@/interface/components/Switch.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
DEFAULT_SIDEBAR_BLUR,
@@ -187,16 +188,24 @@
</div>
</div>
<div class="row" class:disabled={!transparencyOn}>
<div class="row">
<div class="copy">
<h3 class="row-title">Transparency Effects</h3>
<p class="row-desc">Use glass effects on supported surfaces (may impact battery life)</p>
</div>
<div class="control toggle">
<Switch
state={transparencyOn}
onChange={(value) => (settingsState.transparencyEffects = value)}
/>
</div>
</div>
{#if transparencyOn}
<div class="row">
<div class="copy">
<h3 class="row-title">Blur Strength</h3>
<p class="row-desc">
{#if transparencyOn}
Glass blur on the sidebar ({blur}px)
{:else}
Enable Transparency Effects to use blur
{/if}
</p>
<p class="row-desc">Glass blur on the sidebar ({blur}px)</p>
</div>
<div class="control slider">
<Slider
@@ -208,6 +217,7 @@
/>
</div>
</div>
{/if}
</div>
</div>
@@ -538,10 +548,6 @@
border-top: none;
}
.row.disabled {
opacity: 0.55;
}
.copy {
min-width: 0;
flex: 1 1 auto;
@@ -574,4 +580,9 @@
min-width: 10rem;
width: 11rem;
}
.control.toggle {
display: flex;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,87 @@
<script lang="ts">
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import SidebarNavItem from "./SidebarNavItem.svelte";
type NavGroup = {
label?: string;
items: {
id: string;
label: string;
divided?: boolean;
nested?: boolean;
expanded?: boolean;
}[];
};
let { groups, selectedId, onselect } = $props<{
groups: NavGroup[];
selectedId: string;
onselect: (id: string) => void;
}>();
let trackEl = $state<HTMLElement | null>(null);
let indicatorX = $state(0);
let indicatorY = $state(0);
let indicatorW = $state(0);
let indicatorH = $state(40);
let indicatorReady = $state(false);
const updateIndicator = () => {
if (!trackEl) return;
const item = trackEl.querySelector<HTMLElement>(`[data-nav-section="${selectedId}"]`);
if (!item) return;
const trackRect = trackEl.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
indicatorX = itemRect.left - trackRect.left;
indicatorY = itemRect.top - trackRect.top + trackEl.scrollTop;
indicatorW = itemRect.width;
indicatorH = itemRect.height;
indicatorReady = true;
};
$effect(() => {
selectedId;
groups;
trackEl;
queueMicrotask(updateIndicator);
});
</script>
<div
class="relative flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto no-scrollbar"
bind:this={trackEl}
>
{#if indicatorReady}
<div
class="pointer-events-none absolute left-0 top-0 z-0 rounded-lg bg-zinc-200/90 dark:bg-zinc-700/90"
style="transform: translate({indicatorX}px, {indicatorY}px); width: {indicatorW}px; height: {indicatorH}px; transition: {$settingsState.animations
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), width 0.28s cubic-bezier(0.22, 1, 0.36, 1), height 0.28s cubic-bezier(0.22, 1, 0.36, 1)'
: 'none'};"
></div>
{/if}
{#each groups as group}
<div class="relative flex flex-col gap-1">
{#if group.label}
<p
class="relative z-10 mb-1.5 px-3 text-sm font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500"
>
{group.label}
</p>
{/if}
{#each group.items as item (item.id)}
{#if item.divided}
<div class="my-2 border-t border-zinc-200 dark:border-zinc-700 {item.nested ? 'ml-4' : ''}"></div>
{/if}
<SidebarNavItem
label={item.label}
active={selectedId === item.id}
nested={item.nested}
emphasized={item.expanded}
trackId={item.id}
onclick={() => onselect(item.id)}
/>
{/each}
</div>
{/each}
</div>
@@ -0,0 +1,33 @@
<script lang="ts">
let {
label,
active = false,
nested = false,
emphasized = false,
trackId,
onclick,
} = $props<{
label: string;
active?: boolean;
nested?: boolean;
emphasized?: boolean;
trackId?: string;
onclick: () => void;
}>();
</script>
<button
type="button"
data-nav-section={trackId}
{onclick}
class="relative z-10 rounded-lg text-left transition-[color,background-color] duration-200
{nested
? 'ml-4 w-[calc(100%-1rem)] px-3 py-2 text-base'
: 'w-full px-3 py-2.5 text-lg'}
{active || emphasized
? 'font-medium text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-white'}"
aria-current={active ? "page" : undefined}
>
{label}
</button>
@@ -1,3 +0,0 @@
.tab-width {
width: var(--tab-width);
}
@@ -1,6 +1,5 @@
<script lang="ts">
import MotionDiv from './MotionDiv.svelte';
import './TabbedContainer.css';
import { onMount } from 'svelte';
let { tabs, activeTab = $bindable(0) } = $props<{ tabs: { title: string, Content: any, props?: any }[]; activeTab?: number }>();
+1 -1
View File
@@ -3,7 +3,7 @@
.bsplus-font-picker-overlay {
position: fixed;
inset: 0;
z-index: 50000;
z-index: 200001;
display: flex;
align-items: center;
justify-content: center;
@@ -8,11 +8,14 @@
const themeManager = ThemeManager.getInstance();
type Background = { id: string; category: string; type: string; lowResUrl: string; highResUrl: string; name: string; description: string; featured?: boolean };
let { searchTerm } = $props<{ searchTerm: string }>();
let { searchTerm, selectedCategory, onCategoriesChange } = $props<{
searchTerm: string;
selectedCategory: string;
onCategoriesChange: (categories: string[]) => void;
}>();
// Existing states
let backgrounds = $state<Background[]>([]);
let selectedCategory = $state<string>('All');
let error = $state<string | null>(null);
let selectedBackground = $state<string | null>(null);
let isLoading = $state<boolean>(true);
@@ -33,6 +36,7 @@
}
const data = await response.json();
backgrounds = data.backgrounds;
onCategoriesChange([...new Set(backgrounds.map((background) => background.category))]);
debugInfo = `Loaded ${backgrounds.length} backgrounds`;
await loadSavedBackgrounds();
} catch (e) {
@@ -97,7 +101,6 @@
})());
let categories = $derived([...new Set(backgrounds.map(bg => bg.category))]);
// Background management functions
async function saveBackgroundFromUrl(url: string, id: string, fileType: string): Promise<void> {
try {
@@ -162,41 +165,7 @@
}
</script>
<div class="flex h-full">
<!-- Sidebar -->
<div class="p-4 w-64 h-full border-r border-zinc-200 dark:border-zinc-700">
<div class="mb-8">
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
<nav class="space-y-2">
<button
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'All' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
onclick={() => selectedCategory = 'All'}
>
All
</button>
<button
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'Featured' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
onclick={() => selectedCategory = 'Featured'}
>
Featured
</button>
<div class="my-2 border-b border-zinc-200 dark:border-zinc-700"></div>
{#each categories as category}
<button
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === category ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
onclick={() => selectedCategory = category}
>
{category}
</button>
{/each}
</nav>
</div>
</div>
<!-- Main Content -->
<div class="overflow-auto flex-1">
<div class="h-full overflow-auto">
<!-- Header -->
<div class="sticky top-0 z-10 p-4 border-b bg-[#F1F1F3] dark:bg-zinc-900 dark:border-zinc-700">
<div class="flex justify-between items-center mb-4">
@@ -295,7 +264,6 @@
{/if}
</div>
</div>
</div>
{#if settingsState.devMode}
<div class="p-4 mt-8 rounded bg-zinc-100 dark:bg-zinc-800">
@@ -307,4 +275,3 @@
<p>Selected Category: {selectedCategory}</p>
</div>
{/if}
@@ -69,7 +69,7 @@
<button
type="button"
onclick={handleButtonClick}
class="flex items-center gap-2 px-3 py-1.5 text-[0.75rem] rounded-lg shadow-2xl border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 dark:text-white transition-colors duration-200"
class="flex h-12 items-center gap-2 rounded-full bg-zinc-100/80 px-4 text-sm font-medium text-zinc-700 transition-colors duration-150 hover:bg-zinc-200 hover:text-zinc-950 focus:outline-none dark:bg-zinc-900/50 dark:text-zinc-200 dark:hover:bg-zinc-700 dark:hover:text-white"
>
{#if cloudState.isLoggedIn}
{#if cloudState.user?.pfpUrl}
@@ -85,14 +85,14 @@
{/if}
<span
class={alwaysShowUserName
? "inline max-w-[10rem] truncate text-[0.75rem]"
: "hidden max-w-24 truncate sm:inline text-[0.75rem]"}
? "inline max-w-[10rem] truncate"
: "hidden max-w-24 truncate sm:inline"}
>
{cloudState.user?.displayName || cloudState.user?.username || cloudState.user?.email || "User"}
</span>
{:else}
<span class="text-sm font-IconFamily" aria-hidden="true">{'\ued53'}</span>
<span class="text-[0.75rem]">Sign in</span>
<span>Sign in</span>
{/if}
</button>
@@ -34,16 +34,16 @@
</script>
{#if slides.length > 0}
<div class="relative w-full overflow-clip rounded-xl transition-opacity" transition:fade>
<div class="relative w-full overflow-clip rounded-xl ring-1 ring-black/10 transition-opacity dark:ring-white/10" transition:fade>
<div
class="w-full aspect-[5/1] max-h-[500px]"
class="w-full overflow-hidden"
use:emblaCarouselSvelte={{ options, plugins }}
onemblaInit={onInit}
>
<div class="flex">
{#each slides as slide (slide.imageUrl + slide.title + (slide.subtitle ?? ''))}
<div
class="relative flex-[0_0_100%] cursor-pointer rounded-xl overflow-clip"
class="cover-slide relative isolate min-w-0 flex-[0_0_100%] cursor-pointer overflow-hidden"
role="button"
tabindex="0"
onkeydown={(e) => {
@@ -51,7 +51,7 @@
}}
onclick={() => setDisplayTheme(slide.openTheme)}
>
<img src={slide.imageUrl} alt="" class="object-cover w-full h-full" />
<img src={slide.imageUrl} alt="" class="absolute inset-0 z-0 h-full w-full object-cover" />
{#if slide.badgeFeatured === true}
<div class="absolute top-4 left-4 z-[2] pointer-events-none">
<span
@@ -69,7 +69,7 @@
</span>
</div>
{/if}
<div class="absolute bottom-0 left-0 p-8 z-[1]">
<div class="absolute bottom-0 left-0 z-[2] p-8">
<h2 class="text-4xl font-bold text-white">{slide.title}</h2>
{#if slide.subtitle}
<p class="text-lg font-medium text-white/95 mt-1 line-clamp-2">{slide.subtitle}</p>
@@ -81,7 +81,6 @@
<p class="text-lg text-white line-clamp-3">{slide.openTheme.description}</p>
{/if}
</div>
<div class="absolute bottom-0 left-0 w-full h-1/2 to-transparent bg-linear-to-t from-black/80"></div>
</div>
{/each}
</div>
@@ -111,3 +110,26 @@
</div>
</div>
{/if}
<style>
.cover-slide {
aspect-ratio: 3 / 1;
}
.cover-slide::after {
position: absolute;
z-index: 1;
inset: 0;
background: linear-gradient(
to top,
rgb(0 0 0 / 46%) 0%,
rgb(0 0 0 / 42.5%) 18%,
rgb(0 0 0 / 34%) 36%,
rgb(0 0 0 / 21%) 54%,
rgb(0 0 0 / 8%) 72%,
transparent 88%
);
content: '';
pointer-events: none;
}
</style>
+104 -61
View File
@@ -1,77 +1,120 @@
<script lang="ts">
import logo from '@/resources/icons/betterseqta-dark-full.png';
import logoDark from '@/resources/icons/betterseqta-light-full.png';
import browser from 'webextension-polyfill';
import CloudHeader from './CloudHeader.svelte';
import darkLogo from "@/resources/icons/betterseqta-dark-full.png";
import lightLogo from "@/resources/icons/betterseqta-light-full.png";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import MotionDiv from "../MotionDiv.svelte";
import PlainCloseButton from "../PlainCloseButton.svelte";
import CloudHeader from "./CloudHeader.svelte";
const handleCloseStore = () => {
void import('@/seqta/ui/renderStore').then((module) => module.closeStore());
};
type SettingsPage = "settings" | "themes" | "backgrounds";
// Props
let { searchTerm, setSearchTerm, darkMode, activeTab, setActiveTab } = $props<{
searchTerm: string,
setSearchTerm: (term: string) => void,
darkMode: boolean,
activeTab: string,
setActiveTab: (tab: string) => void
let {
searchTerm,
setSearchTerm,
activePage,
setActivePage,
showStoreTools,
onLogoClick,
onClose,
} = $props<{
searchTerm: string;
setSearchTerm: (term: string) => void;
activePage: SettingsPage;
setActivePage: (page: SettingsPage) => void;
showStoreTools: boolean;
onLogoClick: () => void;
onClose: () => void;
}>();
// Clear search input function
const clearSearch = () => {
setSearchTerm('');
};
const tabs: { id: SettingsPage; title: string }[] = [
{ id: "settings", title: "Settings" },
{ id: "themes", title: "Themes" },
{ id: "backgrounds", title: "Backgrounds" },
];
const activeIndex = $derived(tabs.findIndex((tab) => tab.id === activePage));
</script>
<header class="fixed top-0 z-50 w-full h-[4.25rem] bg-white border-b shadow-md border-b-white/10 dark:bg-zinc-950/90 backdrop-blur-xl dark:text-white">
<div class="flex justify-between items-center px-4 py-1">
<div class="flex gap-4 place-items-center cursor-pointer" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
<img src={browser.runtime.getURL(logo)} class="h-14 {darkMode ? 'hidden' : ''}" alt="Logo" />
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
<div class="w-[1px] h-10 my-auto bg-zinc-400 dark:bg-zinc-600"></div>
<button
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'themes' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
onclick={() => setActiveTab('themes')}
<header
class="flex shrink-0 items-center gap-4 border-b border-zinc-200/60 px-5 py-4 dark:border-zinc-700/50"
>
Themes
<button type="button" class="hidden shrink-0 lg:block" onclick={onLogoClick}>
<img
src={resolveExtensionAssetUrl(darkLogo)}
class="h-11 w-52 object-cover dark:hidden"
alt="BetterSEQTA+"
/>
<img
src={resolveExtensionAssetUrl(lightLogo)}
class="hidden h-11 w-52 object-cover dark:block"
alt="BetterSEQTA+"
/>
</button>
<button
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'backgrounds' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
onclick={() => setActiveTab('backgrounds')}
<div
class="h-12 min-w-[14rem] flex-1 rounded-full bg-zinc-100/80 p-1 dark:bg-zinc-900/50"
role="tablist"
aria-label="Settings pages"
>
Backgrounds
<div class="relative flex">
<MotionDiv
class="absolute inset-y-0 left-0 z-0 w-1/3 rounded-full bg-white shadow-sm dark:bg-zinc-700"
animate={{ x: `${activeIndex * 100}%` }}
transition={{ type: "spring", stiffness: 250, damping: 25 }}
/>
{#each tabs as tab (tab.id)}
<button
type="button"
role="tab"
aria-selected={activePage === tab.id}
onclick={() => setActivePage(tab.id)}
class="relative z-10 h-10 flex-1 rounded-full px-4 text-base transition-colors duration-200
{activePage === tab.id
? 'font-semibold text-zinc-900 dark:text-white'
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}"
>
{tab.title}
</button>
{/each}
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
{#if showStoreTools}
<label class="relative hidden w-40 md:block xl:w-56">
<span class="sr-only">Search {activePage}</span>
<svg
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.35-4.35m1.35-5.65a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z" />
</svg>
<input
type="search"
placeholder={activePage === "themes" ? "Search themes" : "Search backgrounds"}
value={searchTerm}
oninput={(event) => setSearchTerm(event.currentTarget.value)}
class="store-search h-12 w-full rounded-full bg-zinc-100/80 pl-9 pr-4 text-sm text-zinc-900 transition-colors duration-150 placeholder:text-zinc-400 focus:bg-zinc-200/70 dark:bg-zinc-900/50 dark:text-white dark:focus:bg-zinc-700"
/>
</label>
<CloudHeader />
</div>
{/if}
<div class="flex relative gap-2">
<input
type="text"
placeholder="Search themes..."
value={searchTerm}
oninput={(e: any) => setSearchTerm(e.target.value)}
class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" />
<svg
class="absolute left-3 top-1/2 w-5 h-5 text-gray-400 transform -translate-y-1/2 dark:text-gray-200"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<!-- Close Button -->
<button
onclick={handleCloseStore}
class="p-1 px-3"
>
<span class="text-2xl font-IconFamily">&#xed8a;</span>
</button>
</div>
<PlainCloseButton onclick={onClose} label="Close settings" />
</div>
</header>
<style>
.store-search,
.store-search:focus,
.store-search:focus-visible {
appearance: none;
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
}
</style>
+36 -46
View File
@@ -4,8 +4,6 @@
masterGridDisplayDownloadCount,
gridCardPreviewImageUrls,
} from '@/interface/utils/themeStoreFlavours'
import { fade } from 'svelte/transition';
import { onMount } from 'svelte';
import emblaCarouselSvelte from 'embla-carousel-svelte';
import Autoplay from 'embla-carousel-autoplay';
let { theme, onClick, toggleFavorite, isLoggedIn, onRequestSignIn, allStoreThemeRows } = $props<{
@@ -49,21 +47,8 @@
];
});
let menuOpen = $state(false);
let menuRef: HTMLDivElement;
onMount(() => {
const closeMenu = (e: MouseEvent) => {
if (menuOpen && menuRef && !menuRef.contains(e.target as Node)) {
menuOpen = false;
}
};
document.addEventListener('click', closeMenu);
return () => document.removeEventListener('click', closeMenu);
});
function handleCardClick(e: MouseEvent) {
if ((e.target as HTMLElement).closest('[data-theme-menu]')) return;
if ((e.target as HTMLElement).closest('[data-theme-favorite]')) return;
onClick();
}
@@ -74,7 +59,6 @@
} else {
onRequestSignIn?.();
}
menuOpen = false;
}
</script>
@@ -86,8 +70,7 @@
onclick={handleCardClick}
>
<div
class="bg-gray-50 w-full transition-all duration-500 ease-out relative group flex flex-col rounded-xl overflow-clip border hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] dark:hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto"
transition:fade
class="theme-card isolate bg-gray-50 w-full transition-[transform,box-shadow,border-color] duration-300 ease-out relative group flex flex-col rounded-xl overflow-clip border hover:scale-[1.02] hover:shadow-xl dark:hover:shadow-black/60 dark:bg-zinc-800 dark:border-white/[0.1] h-auto"
>
{#if theme.featured === true}
<div class="absolute top-2 left-2 z-20 pointer-events-none">
@@ -102,29 +85,21 @@
</span>
</div>
{/if}
<!-- Menu dropdown -->
<div class="absolute top-2 right-2 z-20" data-theme-menu bind:this={menuRef}>
<button
type="button"
class="flex justify-center items-center w-8 h-8 rounded-lg bg-black/40 hover:bg-black/60 text-white transition-all"
onclick={(e) => { e.stopPropagation(); menuOpen = !menuOpen; }}
aria-label="Theme options"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" class="w-5 h-5">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
</svg>
</button>
{#if menuOpen}
<div
class="absolute right-0 top-full mt-1 py-1 min-w-[140px] rounded-lg bg-white dark:bg-zinc-800 shadow-lg border border-zinc-200 dark:border-zinc-700"
role="menu"
>
<button
type="button"
class="flex gap-2 items-center w-full px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700"
role="menuitem"
data-theme-favorite
class="pointer-events-none absolute right-2 top-2 z-20 flex h-10 w-10 scAle-[0.25] items-center justify-center rounded-full bg-black/50 text-white opacity-0 blur-[4px] transition-[opacity,transform,filter,background-color] duration-200 ease-[cubic-bezier(0.2,0,0,1)] hover:bg-black/70 active:scale-[0.96] group-hover:pointer-events-auto group-hover:scale-100 group-hover:opacity-100 group-hover:blur-0 focus-visible:pointer-events-auto focus-visible:scale-100 focus-visible:opacity-100 focus-visible:blur-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
onclick={handleFavoriteClick}
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
aria-label={isLoggedIn
? theme.is_favorited
? 'Remove from favourites'
: 'Add to favourites'
: 'Sign in to favourite themes'}
title={isLoggedIn
? theme.is_favorited
? 'Remove from favourites'
: 'Add to favourites'
: 'Sign in to favourite themes'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -132,15 +107,12 @@
fill={theme.is_favorited ? 'currentColor' : 'none'}
stroke="currentColor"
stroke-width="2"
class="w-5 h-5 {theme.is_favorited ? 'text-red-500' : ''}"
class="h-5 w-5 {theme.is_favorited ? 'text-red-500' : ''}"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 0 1 6.364 0L12 7.636l1.318-1.318a4.5 4.5 0 0 1 6.364 6.364L12 20.364l-7.682-7.682a4.5 4.5 0 0 1 0-6.364Z" />
</svg>
{theme.is_favorited ? 'Favorited' : 'Favorite'}
</button>
</div>
{/if}
</div>
<div class="absolute bottom-1 left-3 right-3 z-10 mb-1 flex flex-col gap-0.5">
<span class="text-xl font-bold text-white drop-shadow-md">{theme.name}</span>
{#if theme.author}
@@ -161,7 +133,6 @@
</span>
</div>
</div>
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></div>
{#if gridRotatorUrls.length === 0}
<div class="relative w-full h-48 overflow-hidden rounded-md bg-zinc-200 dark:bg-zinc-700" aria-hidden="true"></div>
{:else if !allowSlideAutoplay || gridRotatorUrls.length === 1}
@@ -199,3 +170,22 @@
{/if}
</div>
</div>
<style>
.theme-card::after {
position: absolute;
z-index: 1;
inset: 0;
background: linear-gradient(
to top,
rgb(0 0 0 / 92%) 0%,
rgb(0 0 0 / 85%) 18%,
rgb(0 0 0 / 68%) 36%,
rgb(0 0 0 / 42%) 54%,
rgb(0 0 0 / 16%) 72%,
transparent 88%
);
content: '';
pointer-events: none;
}
</style>
@@ -30,7 +30,7 @@
</script>
<div class="relative" >
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
<div class="grid grid-cols-1 gap-4 py-6 mx-auto sm:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)}
<ThemeCard
{theme}
@@ -44,7 +44,7 @@
{#if filteredThemes.length !== 0}
<a href="https://docs.betterseqta.org/theme-creation/" class="block relative z-0 hover:z-20 w-full cursor-pointer">
<div class="bg-zinc-50 h-48 w-full transition-all duration-500 ease-out relative overflow-clip rounded-xl border group group/card flex flex-col justify-center items-center hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1]">
<div class="bg-zinc-50 h-48 w-full transition-[transform,box-shadow,border-color] duration-300 ease-out relative overflow-clip rounded-xl border group group/card flex flex-col justify-center items-center hover:scale-[1.02] hover:shadow-xl dark:hover:shadow-black/60 dark:bg-zinc-800 dark:border-white/[0.1]">
<div class="text-2xl font-IconFamily">{'\uecb3'}</div>
<div class="text-xl font-bold text-center transition-all duration-500 dark:text-white">
Got a Theme Idea?
@@ -12,7 +12,10 @@
const themeManager = ThemeManager.getInstance();
let themes = $state<ThemeList | null>(null);
let { isEditMode } = $props<{ isEditMode: boolean }>();
let { isEditMode, showNavigation = true } = $props<{
isEditMode: boolean;
showNavigation?: boolean;
}>();
let isDragging = $state(false);
let tempTheme = $state(null);
let favoriteStatus = $state<Record<string, boolean>>({});
@@ -128,7 +131,6 @@
const openStorePage = async () => {
const { OpenStorePage } = await import('@/seqta/ui/renderStore')
closeExtensionPopup()
await OpenStorePage()
}
@@ -166,7 +168,7 @@
</script>
<div
class="pt-5 mb-1 w-full"
class="pt-5 mb-1 w-full max-w-lg mx-auto"
role="list"
tabindex="-1"
ondragover={handleDragOver}
@@ -188,7 +190,6 @@
</div>
</div>
</div>
<h2 class="pb-2 text-lg font-bold">Themes</h2>
<div class="flex flex-col gap-2 px-2">
{#if themes}
{#each themes.themes as theme (theme.id)}
@@ -267,10 +268,38 @@
</div>
{/if}
{#if themes && themes.themes.length > 0}
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
{#if themes && themes.themes.length === 0 && !tempTheme}
<div class="flex min-h-56 flex-col items-center justify-center px-4 py-10 text-center">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white" style="text-wrap: balance">
No themes installed
</h3>
<p class="mt-2 max-w-md text-base text-zinc-500 dark:text-zinc-400" style="text-wrap: pretty">
Install a theme from the store or create one of your own.
</p>
<div class="mt-6 grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
<button
type="button"
onclick={() => void openStorePage()}
class="flex h-11 items-center justify-center gap-2 rounded-lg bg-zinc-200 px-4 font-medium text-zinc-900 transition-[background-color,color,transform] duration-150 hover:bg-zinc-300 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600"
>
<span class="font-IconFamily text-lg" aria-hidden="true">&#xecc5;</span>
Install a theme
</button>
<button
type="button"
onclick={() => void openThemeCreator()}
class="flex h-11 items-center justify-center gap-2 rounded-lg bg-zinc-200 px-4 font-medium text-zinc-900 transition-[background-color,color,transform] duration-150 hover:bg-zinc-300 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600"
>
<span class="font-IconFamily text-lg" aria-hidden="true">&#xec60;</span>
Create a theme
</button>
</div>
</div>
{/if}
{#if showNavigation && themes && themes.themes.length > 0}
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
<button
onclick={() => void openStorePage()}
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
@@ -286,6 +315,7 @@
<span class="text-xl font-IconFamily">&#xec60;</span>
<span class="ml-2">Create your own</span>
</button>
{/if}
</div>
</div>
-1
View File
@@ -1,4 +1,3 @@
import "./index.css";
import Settings from "./pages/settings.svelte";
import IconFamily from "@/resources/fonts/IconFamily.woff";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
+8 -6
View File
@@ -16,6 +16,14 @@ export default function renderSvelte(
props: Record<string, any> = {},
shadowStyle: ShadowStyleVariant = "settings",
) {
if (mountPoint instanceof ShadowRoot || props.standalone === true) {
const styleElement = document.createElement("style");
styleElement.textContent = shadowStyles[shadowStyle];
(mountPoint instanceof ShadowRoot ? mountPoint : document.head).appendChild(
styleElement,
);
}
const app = mount(Component, {
target: mountPoint,
props: {
@@ -24,11 +32,5 @@ export default function renderSvelte(
},
});
if (mountPoint instanceof ShadowRoot) {
const styleElement = document.createElement("style");
styleElement.textContent = shadowStyles[shadowStyle];
mountPoint.appendChild(styleElement);
}
return app;
}
+258 -228
View File
@@ -2,7 +2,11 @@
import Settings from "./settings/general.svelte";
import Shortcuts from "./settings/shortcuts.svelte";
import Theme from "./settings/theme.svelte";
import browser from "webextension-polyfill";
import Store from "./store.svelte";
import TabbedContainer from "../components/TabbedContainer.svelte";
import darkLogo from "@/resources/icons/betterseqta-dark-full.png";
import lightLogo from "@/resources/icons/betterseqta-light-full.png";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
import { onMount, onDestroy } from "svelte";
@@ -17,8 +21,15 @@
import CloudPanel from "../components/CloudPanel.svelte";
import DisclaimerModal from "../components/DisclaimerModal.svelte";
import FeedbackModal from "../components/FeedbackModal.svelte";
import SidebarNav from "../components/SidebarNav.svelte";
import StoreHeader from "../components/store/Header.svelte";
import { settingsPopup } from "@/seqta/utils/settingsPopup";
import { consumeOpenFeedbackRequest } from "@/seqta/utils/feedback/client";
import {
consumeSettingsDestination,
SETTINGS_NAVIGATION_EVENT,
type SettingsDestination,
} from "@/seqta/utils/settingsNavigation";
import {
checkGithubReleaseUpdate,
dismissNightlyUpdate,
@@ -26,46 +37,30 @@
isGhReleaseUpdateCheckEnabled,
type GhReleaseUpdateInfo,
} from "@/utils/githubReleaseUpdate";
type PageId = "settings" | "shortcuts" | "themes";
type PageId = "settings" | "themes" | "backgrounds";
type StoreTab = "themes" | "backgrounds";
type ThemeView = "theme-settings" | "theme-store";
type BackgroundView = "background-settings" | "background-store";
type NavItem = {
id: string;
label: string;
divided?: boolean;
nested?: boolean;
expanded?: boolean;
};
const BACKGROUND_CATEGORY_PREFIX = "background-category:";
let devModeSequence = "";
let compactActiveTab = $state(0);
let activePage = $state<PageId>("settings");
let activeSection = $state("general");
let navTrackEl = $state<HTMLElement | null>(null);
let indicatorY = $state(0);
let indicatorH = $state(40);
let indicatorReady = $state(false);
const updateNavIndicator = () => {
if (!navTrackEl || activePage !== "settings") {
indicatorReady = false;
return;
}
const btn = navTrackEl.querySelector<HTMLElement>(
`[data-nav-section="${activeSection}"]`,
);
if (!btn) {
indicatorReady = false;
return;
}
const trackRect = navTrackEl.getBoundingClientRect();
const btnRect = btn.getBoundingClientRect();
indicatorY = btnRect.top - trackRect.top + navTrackEl.scrollTop;
indicatorH = btnRect.height;
indicatorReady = true;
};
$effect(() => {
activeSection;
activePage;
navTrackEl;
queueMicrotask(updateNavIndicator);
});
let activeThemeView = $state<ThemeView>("theme-settings");
let activeBackgroundView = $state<BackgroundView>("background-settings");
let selectedBackgroundCategory = $state("All");
let backgroundCategories = $state<string[]>([]);
let storeSearchTerm = $state("");
let showDisclaimerModal = $state(false);
let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
@@ -75,12 +70,6 @@
const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel();
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 userNav: NavItem[] = [
{ id: "account", label: "My Account" },
{ id: "general", label: "General" },
@@ -92,15 +81,77 @@
{ id: "assessments", label: "Assessments" },
{ id: "features", label: "Features" },
{ id: "advanced", label: "Advanced" },
{ id: "shortcuts", label: "Shortcuts" },
];
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 settingsNavGroups = [
{ label: "User Settings", items: userNav },
{ label: "App Settings", items: appNav },
];
const themeNavGroups = [
{
label: "Themes",
items: [
{ id: "theme-settings", label: "Theme settings" },
{ id: "create-theme", label: "Create theme" },
{ id: "theme-store", label: "Store" },
],
},
];
const backgroundNavGroups = $derived.by(() => {
const storeOpen = activeBackgroundView === "background-store";
const categoryItems: NavItem[] = storeOpen
? ["All", "Featured", ...backgroundCategories].map((category, index) => ({
id: `${BACKGROUND_CATEGORY_PREFIX}${category}`,
label: category,
nested: true,
divided: index === 2,
}))
: [];
return [
{
label: "Backgrounds",
items: [
{ id: "background-settings", label: "Background settings" },
{ id: "background-store", label: "Store", expanded: storeOpen },
...categoryItems,
],
},
];
});
const navGroups = $derived(
activePage === "settings"
? settingsNavGroups
: activePage === "themes"
? themeNavGroups
: backgroundNavGroups,
);
const selectedNavId = $derived(
activePage === "settings"
? activeSection
: activePage === "themes"
? activeThemeView
: activeBackgroundView === "background-store"
? `${BACKGROUND_CATEGORY_PREFIX}${selectedBackgroundCategory}`
: activeBackgroundView,
);
const sectionTitle = $derived.by(() => {
if (activePage === "themes") return "Themes";
if (activePage === "backgrounds") return "Backgrounds";
return [...userNav, ...appNav].find((item) => item.id === activeSection)?.label ?? "Settings";
});
const isStoreView = $derived(
(activePage === "themes" && activeThemeView === "theme-store") ||
(activePage === "backgrounds" && activeBackgroundView === "background-store"),
);
const openGhRelease = () => {
const url =
ghReleaseUpdate?.url ?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
@@ -198,15 +249,52 @@
}
};
const selectPage = (page: PageId) => {
activePage = page;
const selectNavItem = (id: string) => {
if (activePage === "settings") {
activeSection = id;
return;
}
if (activePage === "themes") {
if (id === "create-theme") {
void openThemeCreator();
} else {
activeThemeView = id as ThemeView;
}
return;
}
if (id.startsWith(BACKGROUND_CATEGORY_PREFIX)) {
activeBackgroundView = "background-store";
selectedBackgroundCategory = id.slice(BACKGROUND_CATEGORY_PREFIX.length);
} else {
activeBackgroundView = id as BackgroundView;
}
};
const selectSection = (id: string) => {
activeSection = id;
activePage = "settings";
const openThemeCreator = async () => {
const { OpenThemeCreator } = await import("@/plugins/built-in/themes/ThemeCreator");
OpenThemeCreator();
closeExtensionPopup();
};
const applyDestination = (destination: SettingsDestination) => {
activePage = destination.page;
if (destination.page === "settings" && destination.section) {
activeSection = destination.section;
} else if (destination.page === "themes" && destination.view) {
activeThemeView = destination.view === "store" ? "theme-store" : "theme-settings";
} else if (destination.page === "backgrounds" && destination.view) {
activeBackgroundView = destination.view === "store" ? "background-store" : "background-settings";
}
};
const utilityActions = [
{ label: "About", icon: "\ueb73", onclick: openAbout },
{ label: "What's new", icon: "\ue929", onclick: openChangelog },
{ label: "Privacy", icon: "\uecba", onclick: openPrivacyStatement },
] as const;
onMount(() => {
settingsPopup.addListener(closePopupsOnSettingsClose);
@@ -231,6 +319,14 @@
};
window.addEventListener("bsplus:open-feedback", onOpenFeedback);
const onNavigateSettings = (event: Event) => {
applyDestination((event as CustomEvent<SettingsDestination>).detail);
consumeSettingsDestination();
};
window.addEventListener(SETTINGS_NAVIGATION_EVENT, onNavigateSettings);
const pendingDestination = consumeSettingsDestination();
if (pendingDestination) applyDestination(pendingDestination);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && !standalone) {
closeExtensionPopup();
@@ -241,6 +337,7 @@
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("bsplus:open-feedback", onOpenFeedback);
window.removeEventListener(SETTINGS_NAVIGATION_EVENT, onNavigateSettings);
};
});
@@ -249,126 +346,21 @@
});
</script>
{#snippet navButton(item: NavItem)}
<button
type="button"
data-nav-section={item.id}
onclick={() => selectSection(item.id)}
class="relative z-10 w-full px-3 py-2.5 text-left text-lg rounded-lg transition-colors duration-200
{activePage === 'settings' && activeSection === item.id
? 'text-zinc-900 dark:text-white font-medium'
: 'text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white'}"
>
{item.label}
</button>
{/snippet}
{#snippet settingsShell()}
<div
class="flex flex-col h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white text-[18px] {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_click_events_have_key_events -->
<img
src={browser.runtime.getURL("resources/icons/betterseqta-dark-full.png")}
class="h-9 w-auto dark:hidden shrink-0 cursor-pointer"
alt="BetterSEQTA+"
onclick={handleDevModeToggle}
<StoreHeader
searchTerm={storeSearchTerm}
setSearchTerm={(term) => (storeSearchTerm = term)}
{activePage}
setActivePage={(page) => (activePage = page)}
showStoreTools={isStoreView}
onLogoClick={handleDevModeToggle}
onClose={handleClose}
/>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img
src={browser.runtime.getURL("resources/icons/betterseqta-light-full.png")}
class="hidden h-9 w-auto dark:block shrink-0 cursor-pointer"
alt="BetterSEQTA+"
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.5 text-lg 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}
<div class="flex items-center gap-1 shrink-0">
{#if ghReleaseUpdateEnabled}
<div class="flex flex-col items-end gap-0.5 max-w-[8.5rem] mr-0.5">
{#if ghReleaseUpdate?.available}
<button
type="button"
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 transition-colors duration-200"
title="Open GitHub release"
>
Update — {ghReleaseUpdate.label}
</button>
{/if}
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
{#if ghReleaseChannelLabel}
{ghReleaseChannelLabel} — do not upload to stores.
{:else}
GitHub build — do not upload to stores.
{/if}
</p>
</div>
{/if}
<button
type="button"
onclick={openAbout}
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"
>
&#xeb73;
</button>
<button
type="button"
onclick={openChangelog}
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"
>
&#xe929;
</button>
<button
type="button"
onclick={openPrivacyStatement}
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"
>
&#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>
</div>
{/if}
</div>
<!-- Body: left nav + content -->
<div class="flex flex-1 min-h-0 overflow-hidden">
@@ -378,79 +370,47 @@
: 'w-[260px] px-4 py-5'}"
aria-label="Settings categories"
>
<div class="relative flex flex-col flex-1 min-h-0 gap-5 overflow-y-auto no-scrollbar" bind:this={navTrackEl}>
{#if activePage === "settings" && indicatorReady}
<div
class="absolute left-0 right-0 top-0 z-0 rounded-lg bg-zinc-200/90 dark:bg-zinc-700/90 pointer-events-none"
style="transform: translateY({indicatorY}px); height: {indicatorH}px; transition: {$settingsState.animations
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), height 0.28s cubic-bezier(0.22, 1, 0.36, 1)'
: 'none'};"
></div>
<SidebarNav
groups={navGroups}
selectedId={selectedNavId}
onselect={selectNavItem}
/>
<div class="mt-4 flex shrink-0 flex-col gap-1 border-t border-zinc-200/60 pt-4 dark:border-zinc-700/50">
{#if ghReleaseUpdateEnabled}
<div class="px-3 pb-2">
{#if ghReleaseUpdate?.available}
<button
type="button"
onclick={openGhRelease}
class="mb-1 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"
>
Update — {ghReleaseUpdate.label}
</button>
{/if}
<p class="text-[9px] leading-tight text-zinc-500 dark:text-zinc-400">
{ghReleaseChannelLabel ?? "GitHub build"} — do not upload to stores.
</p>
</div>
{/if}
{#if activePage === "settings"}
<div class="flex flex-col gap-1">
<p
class="relative z-10 px-3 mb-1.5 text-sm 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 class="flex flex-col gap-1">
<p
class="relative z-10 px-3 mb-1.5 text-sm 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>
{:else if activePage === "shortcuts"}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Shortcuts
</p>
{#each utilityActions as action (action.label)}
<button
type="button"
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
onclick={action.onclick}
class="flex h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-[16px] font-medium text-zinc-600 transition-[color,background-color,transform] duration-150 hover:bg-zinc-200/70 hover:text-zinc-950 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:text-zinc-300 dark:hover:bg-zinc-700/70 dark:hover:text-white dark:focus:ring-offset-zinc-900"
>
Shortcuts
<span class="w-5 text-center font-IconFamily text-[18px]" aria-hidden="true">{action.icon}</span>
<span>{action.label}</span>
</button>
</div>
{:else}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Themes
</p>
<button
type="button"
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
>
Themes
</button>
</div>
{/if}
</div>
{/each}
<button
type="button"
onclick={() => openFeedback()}
class="shrink-0 mt-4 w-full px-3 py-2.5 text-left text-[18px] font-medium rounded-lg transition-all duration-200
text-zinc-700 dark:text-zinc-200
bg-zinc-200/70 dark:bg-zinc-800/80
hover:bg-zinc-300/80 dark:hover:bg-zinc-700
hover:scale-[1.02] active:scale-95
focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-900"
class="mt-2 flex w-full items-center gap-3 rounded-lg bg-zinc-200/70 px-3 py-2.5 text-left text-[16px] font-medium text-zinc-700 transition-[color,background-color,transform] duration-150 hover:bg-zinc-300/80 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:bg-zinc-700 dark:focus:ring-offset-zinc-900"
>
<span class="flex items-center gap-2">
<svg
class="w-5 h-5 shrink-0"
viewBox="0 0 24 24"
@@ -464,10 +424,26 @@
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<span>Send us feedback!</span>
</span>
</button>
</div>
</nav>
{#if isStoreView}
<div class="min-w-0 min-h-0 flex-1">
<Store
activeTab={activePage as StoreTab}
searchTerm={storeSearchTerm}
{selectedBackgroundCategory}
setActiveTab={(tab) => {
activePage = tab;
if (tab === "themes") activeThemeView = "theme-store";
else activeBackgroundView = "background-store";
}}
setSearchTerm={(term) => (storeSearchTerm = term)}
setBackgroundCategories={(categories) => (backgroundCategories = categories)}
/>
</div>
{:else}
<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-3xl font-semibold tracking-tight text-zinc-900 dark:text-white">
@@ -476,6 +452,9 @@
</div>
<div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar">
{#if activePage === "settings"}
{#if activeSection === "shortcuts"}
<Shortcuts />
{:else}
<Settings
showColourPicker={openColourPicker}
showFontPicker={openFontPicker}
@@ -483,13 +462,63 @@
showCloudPanel={openCloudPanel}
{activeSection}
/>
{:else if activePage === "shortcuts"}
<Shortcuts />
{/if}
{:else if activePage === "themes"}
<Theme section="themes" />
{:else}
<Theme />
<Theme section="backgrounds" />
{/if}
</div>
</div>
{/if}
</div>
</div>
{/snippet}
{#snippet compactSettings()}
<div
class="flex h-full min-h-0 flex-col gap-2 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white"
>
<div
class="grid shrink-0 place-items-center border-b border-zinc-200/40 dark:border-zinc-700/40"
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img
src={resolveExtensionAssetUrl(darkLogo)}
class="w-4/5 dark:hidden"
alt="BetterSEQTA+"
onclick={handleDevModeToggle}
/>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img
src={resolveExtensionAssetUrl(lightLogo)}
class="hidden w-4/5 dark:block"
alt="BetterSEQTA+"
onclick={handleDevModeToggle}
/>
</div>
<div class="min-h-0 flex-1 overflow-hidden">
<TabbedContainer
bind:activeTab={compactActiveTab}
tabs={[
{
title: "Settings",
Content: Settings,
props: {
showColourPicker: openColourPicker,
showFontPicker: openFontPicker,
showDisclaimer,
showCloudPanel: openCloudPanel,
activeSection: "all",
},
},
{ title: "Shortcuts", Content: Shortcuts },
{ title: "Themes", Content: Theme },
]}
/>
</div>
</div>
{/snippet}
@@ -500,7 +529,7 @@
? 'dark'
: ''}"
>
{@render settingsShell()}
{@render compactSettings()}
</div>
{:else}
<div
@@ -520,6 +549,7 @@
<div
class="relative z-10 w-[min(1180px,96vw)] h-[min(860px,92vh)] no-scrollbar overflow-clip"
data-settings-panel
>
{@render settingsShell()}
</div>
+9 -15
View File
@@ -160,7 +160,11 @@
notificationCollector: "features",
};
const showsSection = (section: string) =>
activeSection === "all" || activeSection === section;
const pluginBelongsInSection = (pluginId: string) =>
activeSection === "all" ||
(pluginSectionById[pluginId] ?? "features") === activeSection;
@@ -191,7 +195,7 @@
{/snippet}
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
{#if activeSection === "account"}
{#if showsSection("account")}
{@render Setting({
title: "Connect Mobile App",
description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn",
@@ -220,7 +224,7 @@
</div>
{/if}
{#if activeSection === "general"}
{#if showsSection("general")}
{#each [
...(!isEngage
? [
@@ -266,16 +270,6 @@
onChange: (isOn: boolean) => settingsState.timeFormat = isOn ? "12" : "24"
}
},
{
title: "Transparency Effects",
description: "Enable transparency effects on certain elements, such as blur (May impact battery life)",
id: 1,
Component: Switch,
props: {
state: $settingsState.transparencyEffects,
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
}
},
{
title: "Default Page",
description: "Choose which page loads first when you open SEQTA",
@@ -335,7 +329,7 @@
{/each}
{/if}
{#if activeSection === "appearance"}
{#if showsSection("appearance")}
{#each [
{
title: "Custom Theme Colour",
@@ -410,7 +404,7 @@
</div>
{/if}
{#if activeSection === "general"}
{#if showsSection("general")}
<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">
@@ -590,7 +584,7 @@
{/if}
{/each}
{#if activeSection === "advanced"}
{#if showsSection("advanced")}
{@render Setting({
title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features",
+18 -3
View File
@@ -3,21 +3,32 @@
import ThemeSelector from "@/interface/components/themes/ThemeSelector.svelte"
import { standalone } from "@/interface/utils/standalone.svelte"
let { section = "all" } = $props<{
section?: "all" | "themes" | "backgrounds";
}>();
// backgrounds
let selectedBackground = $state<string | null>(null);
let selectNoBackground = $state<() => void>(() => { });
let clearTheme = $derived(selectedBackground !== null);
let editMode = $state<boolean>(false);
let showBackgrounds = $derived(section !== "themes");
let showThemes = $derived(section !== "backgrounds");
</script>
<div class="py-4">
{#if !standalone.standalone}
{#if showBackgrounds}
<button
onclick={() => selectNoBackground()}
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
{ clearTheme ? 'Clear Theme' : 'Select a Theme' }
disabled={section === "backgrounds" && !clearTheme}
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50 disabled:opacity-50">
{section === "all"
? clearTheme ? 'Clear Theme' : 'Select a Theme'
: clearTheme ? 'Clear Background' : 'No Background Selected'}
</button>
{/if}
<div class="relative w-full">
<button
onclick={() => editMode = !editMode}
@@ -26,8 +37,12 @@
<span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span>
</button>
{#if showBackgrounds}
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
<ThemeSelector isEditMode={editMode} />
{/if}
{#if showThemes}
<ThemeSelector isEditMode={editMode} showNavigation={section === "all"} />
{/if}
</div>
{:else}
<div class="flex justify-center items-center w-full h-full">
+28 -26
View File
@@ -5,12 +5,10 @@
import CoverSwiper from '../components/store/CoverSwiper.svelte';
import ThemeGrid from '../components/store/ThemeGrid.svelte';
import SkeletonLoader from '../components/SkeletonLoader.svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import type { Theme } from '../types/Theme'
import { visibleStoreThemes, buildCoverSlidesForThemes, normalizeStoreTheme } from '@/interface/utils/themeStoreFlavours'
import browser from 'webextension-polyfill'
import ThemeModal from '../components/store/ThemeModal.svelte'
import Header from '../components/store/Header.svelte'
import { themeUpdates } from '../hooks/ThemeUpdates'
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
@@ -21,6 +19,22 @@
import { consumePendingHighlightThemeId } from '@/seqta/utils/openThemeStoreWithHighlight'
const themeManager = ThemeManager.getInstance();
type StoreTab = 'themes' | 'backgrounds';
let {
activeTab,
searchTerm,
selectedBackgroundCategory,
setActiveTab,
setSearchTerm,
setBackgroundCategories,
} = $props<{
activeTab: StoreTab;
searchTerm: string;
selectedBackgroundCategory: string;
setActiveTab: (tab: StoreTab) => void;
setSearchTerm: (term: string) => void;
setBackgroundCategories: (categories: string[]) => void;
}>();
let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn);
$effect(() => {
@@ -29,7 +43,6 @@
});
// State variables
let searchTerm = $state('');
let themes = $state<Theme[]>([]);
/** Grid/search/cover: hides flat-listed slaves when API sends them */
@@ -38,10 +51,8 @@
/** Cover marquee slides (master + flavour imagery for top masters) */
let coverSlides = $derived(buildCoverSlidesForThemes(listThemes.slice(0, 3)));
let loading = $state(true);
let darkMode = $state(false);
let displayTheme = $state<Theme | null>(null);
let currentThemes = $state<string[]>([]);
let activeTab = $state('themes');
let error = $state<string | null>(null);
let fetchAttempt = $state(0);
@@ -69,14 +80,6 @@
displayTheme = theme;
};
const setSearchTerm = (term: string) => {
searchTerm = term;
};
const setActiveTab = (tab: string) => {
activeTab = tab;
};
/** Featured themes first; within each group, newest by `created_at` (API: Unix seconds). */
function compareStoreThemes(a: Theme, b: Theme): number {
const fa = a.featured === true ? 1 : 0;
@@ -155,8 +158,8 @@
const match = themes.find((t) => t.id === themeId)
?? themes.find((t) => t.flavours?.some((f) => f.id === themeId));
if (match) {
activeTab = 'themes';
searchTerm = '';
setActiveTab('themes');
setSearchTerm('');
displayTheme = match;
}
}
@@ -175,9 +178,6 @@
await fetchThemes();
await fetchCurrentThemes();
darkMode = (await browser.storage.local.get('DarkMode')).DarkMode === 'true';
darkMode = $settingsState.DarkMode;
const pending = consumePendingHighlightThemeId();
if (pending) focusThemeById(pending);
@@ -242,11 +242,9 @@
});
</script>
<div class="w-screen h-screen bg-white {darkMode ? 'dark' : ''}">
<div class="h-full overflow-y-scroll bg-zinc-200/50 dark:bg-zinc-900 dark:text-white pt-[4.25rem]">
<Header {searchTerm} {setSearchTerm} {darkMode} {activeTab} {setActiveTab} />
<div class={`px-12 h-full ${activeTab === 'backgrounds' ? 'pt-0' : 'pt-6 md:px-24 lg:px-48'}`}>
<div class="relative flex h-full min-h-0 flex-col overflow-hidden text-zinc-900 dark:text-white">
<main class="min-h-0 flex-1 overflow-y-auto bg-zinc-50/80 dark:bg-zinc-900/40">
<div class={activeTab === 'backgrounds' ? 'h-full' : 'px-6 py-6 md:px-8 lg:px-10'}>
<!-- Loading State -->
{#if loading}
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
@@ -263,7 +261,7 @@
</p>
<button
type="button"
class="mt-6 px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700"
class="mt-6 rounded-lg bg-zinc-900 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white"
onclick={() => {
loading = true;
error = null;
@@ -312,11 +310,15 @@
/>
{/if}
{:else if activeTab === 'backgrounds'}
<Backgrounds {searchTerm} />
<Backgrounds
{searchTerm}
selectedCategory={selectedBackgroundCategory}
onCategoriesChange={setBackgroundCategories}
/>
{/if}
{/if}
</div>
</div>
</main>
{#if showSignInOverlay}
<SignInToFavoriteModal onClose={() => (showSignInOverlay = false)} />
@@ -75,12 +75,12 @@
}
p {
display: flex;
align-self: stretch;
align-items: center;
font-size: 14px;
margin-left: 8px;
margin-right: 48px;
height: 100%;
margin-bottom: 0;
line-height: 32px;
margin: 0 48px 0 8px;
line-height: 1;
font-weight: 400;
}
}
@@ -146,7 +146,7 @@
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
transform: translateX(300%);
}
}
+20 -23
View File
@@ -167,6 +167,7 @@ const messageFoldersPlugin = {
let activeFolderId: string | null = null;
let messageListObserver: MutationObserver | null = null;
let observedMessageList: Element | null = null;
let sidebarObserver: MutationObserver | null = null;
let actionsObserver: MutationObserver | null = null;
let openDropdown: HTMLElement | null = null;
@@ -218,9 +219,6 @@ const messageFoldersPlugin = {
}
};
const isMessageInAnyCustomFolder = (messageId: string): boolean =>
getAssignedFolderIds(messageId, getAssignments()).length > 0;
const shouldShowBadgesInList = (): boolean => {
return api.settings.showTagsInAllMessages || activeFolderId !== null;
};
@@ -837,20 +835,9 @@ const messageFoldersPlugin = {
const messageItems = getMessageListItems();
const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button");
if (activeFolderId === null) {
if (api.settings.hideFolderedMessagesInAll) {
for (const li of messageItems) {
const msgId = li.getAttribute("data-message");
if (msgId && isMessageInAnyCustomFolder(msgId)) {
li.classList.add("bsplus-folder-hidden");
} else {
li.classList.remove("bsplus-folder-hidden");
}
}
} else {
for (const li of messageItems) {
li.classList.remove("bsplus-folder-hidden");
}
}
if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden");
return;
}
@@ -868,14 +855,29 @@ const messageFoldersPlugin = {
const setupMessageListObserver = () => {
const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol");
if (!messageList || messageListObserver) return;
messageListObserver = new MutationObserver(() => {
if (!messageList || messageList === observedMessageList) return;
messageListObserver?.disconnect();
observedMessageList = messageList;
messageListObserver = new MutationObserver((mutations) => {
const messageRowsChanged = mutations.some((mutation) =>
mutation.type === "childList"
? mutation.target === messageList
: mutation.target instanceof Element &&
mutation.target.parentElement === messageList &&
mutation.target.matches("li[data-message]"),
);
if (!messageRowsChanged) return;
applyBadges();
applyFolderFilter();
attachDragListeners();
attachContextMenuListeners();
});
messageListObserver.observe(messageList, { childList: true, subtree: false });
messageListObserver.observe(messageList, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class", "data-message"],
});
};
const attachContextMenuListeners = () => {
@@ -925,6 +927,7 @@ const messageFoldersPlugin = {
renderSidebarFolders();
attachNativeSidebarListeners();
}
setupMessageListObserver();
});
sidebarObserver.observe(sidebar, { childList: true, subtree: true });
}
@@ -937,12 +940,6 @@ const messageFoldersPlugin = {
applyBadges();
}),
);
unregisters.push(
api.settings.onChange("hideFolderedMessagesInAll", () => {
applyFolderFilter();
}),
);
return () => {
for (const u of unregisters) u.unregister();
messageListObserver?.disconnect();
@@ -9,12 +9,6 @@ const settings = defineSettings({
description:
"When off, folder tags are not shown on the message list until you select a folder.",
}),
hideFolderedMessagesInAll: booleanSetting({
default: true,
title: "Hide foldered messages in All Messages",
description:
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
}),
});
export default defineLazyPlugin({
-1
View File
@@ -27,7 +27,6 @@ export const SYNCABLE_PLUGIN_SETTING_DEFAULTS: Record<
"background-music": { volume: 0.5, pauseOnHidden: true },
messageFolders: {
showTagsInAllMessages: true,
hideFolderedMessagesInAll: true,
},
"enhanced-navigation": { autoScrollOnClick: false },
"global-search": {
+6 -36
View File
@@ -1,39 +1,9 @@
import { unmount } from "svelte";
let remove: () => void;
export async function OpenStorePage(): Promise<void> {
remove = await renderStore();
}
export async function renderStore() {
const [{ default: renderSvelte }, { default: Store }] = await Promise.all([
import("@/interface/main"),
import("@/interface/pages/store.svelte"),
const [{ requestSettingsDestination }, { openSettingsPopup }] =
await Promise.all([
import("@/seqta/utils/settingsNavigation"),
import("@/seqta/utils/setupSettingsButton"),
]);
const container =
document.querySelector("#container") ??
document.getElementById("content") ??
document.body;
document.getElementById("store")?.remove();
const child = document.createElement("div");
child.id = "store";
container.appendChild(child);
const shadow = child.attachShadow({ mode: "open" });
const app = renderSvelte(Store, shadow);
return () => unmount(app);
}
export function closeStore() {
document.getElementById("store")!.classList.add("hide");
setTimeout(() => {
remove();
document.getElementById("store")!.remove();
}, 500);
requestSettingsDestination({ page: "themes", view: "store" });
await openSettingsPopup();
}
@@ -52,7 +52,12 @@ async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
const mount = () => renderSvelte(Settings, shadow);
if ("requestIdleCallback" in window) {
requestIdleCallback(mount);
await new Promise<void>((resolve) => {
requestIdleCallback(() => {
mount();
resolve();
});
});
} else {
mount();
}
+12 -3
View File
@@ -11,14 +11,23 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
extensionPopup.classList.add("hide");
if (settingsState.animations) {
const panel = extensionPopup.shadowRoot?.querySelector<HTMLElement>(
"[data-settings-panel]",
);
animate(1, 0, {
onUpdate: (progress) => {
extensionPopup.style.opacity = Math.max(0, progress).toString();
},
type: "spring",
stiffness: 520,
damping: 20,
duration: 0.18,
ease: "easeIn",
});
if (panel) {
animate(
panel,
{ scale: [1, 0], opacity: [1, 0] },
{ duration: 0.18, ease: "easeIn" },
);
}
} else {
extensionPopup.style.opacity = "0";
}
@@ -311,10 +311,10 @@ export async function loadEngageHomePage(): Promise<void> {
const engageHomeBody = stringToHTML(/* html */ `
<div class="home-root" id="engage-home-root">
<div class="home-container" id="engage-home-container">
<div class="bsplus-rounded shortcut-container">
<div class="bsplus-rounded shortcuts" id="shortcuts"></div>
<div class="border shortcut-container">
<div class="border shortcuts" id="shortcuts"></div>
</div>
<div class="bsplus-rounded timetable-container">
<div class="border timetable-container">
<div class="home-subtitle">
<div class="engage-timetable-title-cluster">
<h2 id="engage-home-lesson-subtitle">Today's Lessons</h2>
@@ -331,7 +331,7 @@ export async function loadEngageHomePage(): Promise<void> {
</div>
<div class="day-container loading" id="engage-day-container"></div>
</div>
<div class="bsplus-rounded notices-container">
<div class="border notices-container">
<div style="display: flex; justify-content: space-between">
<h2 class="home-subtitle">Notices</h2>
<input type="date" id="engage-notices-date" />
+5 -5
View File
@@ -62,10 +62,10 @@ export async function loadHomePage() {
const skeletonStructure = stringToHTML(/* html */ `
<div class="home-container" id="home-container">
<div class="bsplus-rounded shortcut-container">
<div class="bsplus-rounded shortcuts" id="shortcuts"></div>
<div class="border shortcut-container">
<div class="border shortcuts" id="shortcuts"></div>
</div>
<div class="bsplus-rounded timetable-container">
<div class="border timetable-container">
<div class="home-subtitle">
<h2 id="home-lesson-subtitle">Today's Lessons</h2>
<div class="timetable-arrows">
@@ -80,7 +80,7 @@ export async function loadHomePage() {
<div class="day-container loading" id="day-container">
</div>
</div>
<div class="bsplus-rounded upcoming-container">
<div class="border upcoming-container">
<div class="upcoming-title">
<h2 class="home-subtitle">Upcoming Assessments</h2>
<div class="upcoming-filters" id="upcoming-filters"></div>
@@ -88,7 +88,7 @@ export async function loadHomePage() {
<div class="upcoming-items loading" id="upcoming-items">
</div>
</div>
<div class="bsplus-rounded notices-container">
<div class="border notices-container">
<div style="display: flex; justify-content: space-between">
<h2 class="home-subtitle">Notices</h2>
<input type="date" />
@@ -11,15 +11,9 @@ export function consumePendingHighlightThemeId(): string | null {
export async function openThemeStoreWithHighlight(themeId: string): Promise<void> {
pendingHighlightThemeId = themeId;
const existing = document.getElementById("store");
if (existing) {
const { OpenStorePage } = await import("@/seqta/ui/renderStore");
await OpenStorePage();
window.dispatchEvent(
new CustomEvent("bsplus:highlight-theme", { detail: { themeId } }),
);
return;
}
const { OpenStorePage } = await import("@/seqta/ui/renderStore");
await OpenStorePage();
}
+20
View File
@@ -0,0 +1,20 @@
export type SettingsDestination =
| { page: "settings"; section?: string }
| { page: "themes" | "backgrounds"; view?: "settings" | "store" };
export const SETTINGS_NAVIGATION_EVENT = "bsplus:navigate-settings";
let pendingDestination: SettingsDestination | null = null;
export function requestSettingsDestination(destination: SettingsDestination): void {
pendingDestination = destination;
window.dispatchEvent(
new CustomEvent(SETTINGS_NAVIGATION_EVENT, { detail: destination }),
);
}
export function consumeSettingsDestination(): SettingsDestination | null {
const destination = pendingDestination;
pendingDestination = null;
return destination;
}
+26 -7
View File
@@ -17,7 +17,18 @@ export function setupSettingsButton() {
AddedSettings.dataset.bsplusSettingsBound = "1";
AddedSettings.addEventListener("click", async () => {
// Re-query each click — Engage SPA navigations can recreate the host.
if (SettingsClicked) {
closeExtensionPopup();
} else {
await openSettingsPopup();
}
});
}
export async function openSettingsPopup(): Promise<void> {
if (SettingsClicked && document.getElementById("ExtensionPopup")) return;
// Re-query each time — Engage SPA navigations can recreate the host.
let extensionPopup = document.getElementById("ExtensionPopup");
if (!extensionPopup) {
const { addExtensionSettings } = await import("./Adders/AddExtensionSettings");
@@ -26,14 +37,13 @@ export function setupSettingsButton() {
}
if (!extensionPopup) return;
if (SettingsClicked) {
closeExtensionPopup(extensionPopup);
} else {
await renderSettingsIfNeeded();
await delay(30);
extensionPopup.style.transform = "none";
const panel = extensionPopup.shadowRoot?.querySelector<HTMLElement>(
"[data-settings-panel]",
);
if (settingsState.animations) {
animate(0, 1, {
onUpdate: (progress) => {
@@ -43,12 +53,21 @@ export function setupSettingsButton() {
stiffness: 280,
damping: 20,
});
if (panel) {
animate(
panel,
{ scale: [0, 1], opacity: [0, 1] },
{ type: "spring", stiffness: 330, damping: 30 },
);
}
} else {
extensionPopup.style.opacity = "1";
extensionPopup.style.transition = "opacity 0s linear";
if (panel) {
panel.style.scale = "1";
panel.style.opacity = "1";
}
}
extensionPopup.classList.remove("hide");
changeSettingsClicked(true);
}
});
}