From e732011f1edb4780a4d6affa9f4ad6eeed0fc960 Mon Sep 17 00:00:00 2001 From: Aden Linday Date: Tue, 21 Jul 2026 09:20:53 +0930 Subject: [PATCH] feat: sidebar cleanup + fixes --- src/css/injected.scss | 57 ++++--- src/css/injected/sidebar-animation.scss | 6 +- .../gradeAnalytics/injectAnalyticsMenuItem.ts | 9 +- src/seqta/ui/colors/Manager.ts | 11 +- src/seqta/ui/sidebar/Sidebar.svelte | 89 ++++------ src/seqta/ui/sidebar/SidebarItem.svelte | 15 +- src/seqta/ui/sidebar/index.ts | 2 - src/seqta/ui/sidebar/mountCustomSidebar.ts | 90 ++++------ src/seqta/ui/sidebar/parseNativeMenu.ts | 14 +- src/seqta/ui/sidebar/sidebarState.svelte.ts | 154 ++++++++++++------ src/seqta/ui/sidebar/types.ts | 1 - 11 files changed, 218 insertions(+), 230 deletions(-) diff --git a/src/css/injected.scss b/src/css/injected.scss index dccad4a4..de253f59 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -635,16 +635,14 @@ html.bsplus-custom-sidebar-pending #menu > .icon-cover, clip: rect(0, 0, 0, 0) !important; } -/* Custom Svelte sidebar: SEQTA-like `#menu > ul > li.item` / `.sub` for theme CSS. */ +/* Custom Svelte sidebar: keep `#menu > ul > li.item` / `.sub` shape for theme CSS. */ #menu.bsplus-custom-sidebar { - /* SEQTA uses absolute top/bottom:0 — do not switch to relative or height collapses. */ position: absolute !important; top: 0 !important; bottom: 0 !important; left: 0 !important; display: flex !important; flex-direction: column !important; - /* No padding-top: logo `ul::before` + theme `.sub { top: 71.5px }` match native. */ box-sizing: border-box !important; height: auto !important; max-height: none !important; @@ -659,23 +657,20 @@ html.bsplus-custom-sidebar-pending #menu > .icon-cover, position: relative; } - /* Theme drill panels assume static hasChildren + absolute .sub filling #menu. */ li.hasChildren { position: static !important; } - /* - * Native SEQTA leaves `li.hasChildren.active` on the hidden list after click-through. - * Global `#menu:has(> ul > li.hasChildren.active) > ul > li:not(.hasChildren.active)` - * then sets pointer-events:none on our custom root rows. Override that here; drill - * locking only follows the custom list's own active folder. - */ + /* Native list may keep `.active` after click-through; force our rows clickable, + then lock siblings only while a custom drill `.sub` is open. */ > #bsplus-sidebar-root > li.item { pointer-events: auto !important; + width: auto !important; + max-width: none !important; + margin: 2px 6px !important; + box-sizing: border-box !important; } - /* Only lock siblings while a drill `.sub` is open — not when a folder is - merely route-highlighted with `.active` at the root list. */ &:has(> #bsplus-sidebar-root > li.hasChildren.active > .sub) > #bsplus-sidebar-root > li:not(.hasChildren.active), @@ -692,40 +687,42 @@ html.bsplus-custom-sidebar-pending #menu > .icon-cover, > #bsplus-sidebar-root li.hasChildren.active > .sub { pointer-events: auto; - /* Let the #menu chrome (gradient / theme) show through — themes often paint - `.sub` with flat `--background-primary` which reads as a white slab. */ background: transparent !important; background-image: none !important; - animation: bsplus-submenu-enter 0.32s cubic-bezier(0.22, 1, 0.36, 1); } > #bsplus-sidebar-root li.hasChildren.active > .sub:has(.hasChildren.active) { pointer-events: none !important; - animation: none; } > #bsplus-sidebar-root li.hasChildren.active .hasChildren.active > .sub { pointer-events: auto !important; background: transparent !important; background-image: none !important; + } + + /* One-shot enter — not tied to `.active` (SEQTA strips/restores that on every + nested click, which would restart the animation). */ + > #bsplus-sidebar-root .sub.bsplus-sub-enter { animation: bsplus-submenu-enter 0.32s cubic-bezier(0.22, 1, 0.36, 1); } - /* Keep folder + leaf rows on the same rhythm (themes often only style - `:not(.hasChildren)`, which left folders centred / differently spaced). */ - > #bsplus-sidebar-root li.item { - width: auto !important; - max-width: none !important; - margin: 2px 6px !important; - box-sizing: border-box !important; - } - - /* Folder label stays in flow so sidebar-animation translateX can run. - Visually covered by the absolute `.sub` panel. */ > #bsplus-sidebar-root > li.hasChildren.active > label { pointer-events: none !important; } + /* Theme decorations on root leaves (Beach palm/sand) must not show through + the transparent drill `.sub` panel. */ + > #bsplus-sidebar-root:has(> li.hasChildren.active > .sub) + > li:not(.hasChildren.active)::before, + > #bsplus-sidebar-root:has(> li.hasChildren.active > .sub) + > li:not(.hasChildren.active)::after { + content: none !important; + display: none !important; + animation: none !important; + opacity: 0 !important; + } + #bsplus-sidebar-root .sub .back { display: flex; align-items: center; @@ -849,6 +846,12 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) { display: none !important; } + /* Compact BetterSEQTA mark instead of the full wordmark. */ + &.student #menu > ul::before { + background-size: 36px 36px !important; + background-position: center center !important; + } + } [class*="notifications__items___"] { -ms-overflow-style: none !important; diff --git a/src/css/injected/sidebar-animation.scss b/src/css/injected/sidebar-animation.scss index 88cb2e2e..f789b142 100644 --- a/src/css/injected/sidebar-animation.scss +++ b/src/css/injected/sidebar-animation.scss @@ -8,12 +8,16 @@ } #menu > ul:has(li.hasChildren.active) > li::before, +#menu > ul:has(li.hasChildren.active) > li::after, #menu > ul ul:has(li.hasChildren.active) > li::before, +#menu > ul ul:has(li.hasChildren.active) > li::after, #menu > ul:has(li.hasChildren.active) > li > label, #menu > ul:has(li.hasChildren.active) > li > svg, #menu > ul ul:has(li.hasChildren.active) > li > label, #menu > ul ul:has(li.hasChildren.active) > li > svg { - transform: translateX(-320px); + /* !important beats theme animations (e.g. Beach palm-slide forwards). */ + transform: translateX(-320px) !important; + animation: none !important; } #menu li.hasChildren:not(.active) li .sub { diff --git a/src/plugins/built-in/gradeAnalytics/injectAnalyticsMenuItem.ts b/src/plugins/built-in/gradeAnalytics/injectAnalyticsMenuItem.ts index 152b80d3..80c62547 100644 --- a/src/plugins/built-in/gradeAnalytics/injectAnalyticsMenuItem.ts +++ b/src/plugins/built-in/gradeAnalytics/injectAnalyticsMenuItem.ts @@ -10,19 +10,12 @@ import { ChangeMenuItemPositions } from "@/seqta/utils/Openers/menuOrder"; import { isMenuOptionsOpen } from "@/seqta/utils/Openers/menuOptionsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { applyMenuItemVisibility } from "@/seqta/utils/menuItemVisibility"; +import { getNativeMenuList } from "@/seqta/ui/sidebar/parseNativeMenu"; const ANALYTICS_MENU_ICON = MenuitemSVGKey.analytics; export const ANALYTICS_MENU_CLASS = "betterseqta-grade-analytics-item"; export const ANALYTICS_MENU_KEY = "analytics"; -function getNativeMenuList(): HTMLElement | null { - const menu = document.getElementById("menu"); - if (!menu) return null; - return menu.querySelector( - ":scope > ul:not(#bsplus-sidebar-root)", - ) as HTMLElement | null; -} - function loadAnalyticsPageLazy() { void import("./loadAnalyticsPage").then((m) => m.loadAnalyticsPage()); } diff --git a/src/seqta/ui/colors/Manager.ts b/src/seqta/ui/colors/Manager.ts index e0805280..bb55220c 100644 --- a/src/seqta/ui/colors/Manager.ts +++ b/src/seqta/ui/colors/Manager.ts @@ -10,6 +10,8 @@ import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/cus import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl"; import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; +import darkIcon from "@/resources/icons/betterseqta-light-icon.png"; +import lightIcon from "@/resources/icons/betterseqta-dark-icon.png"; const ADAPTIVE_THEME_TRANSITION_MS = 400; const LOGO_STYLE_ID = "bsplus-logo-style"; @@ -88,7 +90,8 @@ function cancelColorTransition() { /** Chromium does not always resolve extension URLs inside CSS variables on ::before. */ function applyBetterseqtaLogoBackground(isDark: boolean) { - const url = resolveExtensionAssetUrl(isDark ? darkLogo : lightLogo); + const fullUrl = resolveExtensionAssetUrl(isDark ? darkLogo : lightLogo); + const iconUrl = resolveExtensionAssetUrl(isDark ? darkIcon : lightIcon); let styleEl = document.getElementById(LOGO_STYLE_ID); if (!styleEl) { styleEl = document.createElement("style"); @@ -98,7 +101,11 @@ function applyBetterseqtaLogoBackground(isDark: boolean) { styleEl.textContent = ` body.student #menu > ul::before, #title::before { - background-image: url("${url}") !important; + background-image: url("${fullUrl}") !important; + } + /* Icon-only sidebar: wordmark → compact mark (theme-aware). */ + body.student.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul::before { + background-image: url("${iconUrl}") !important; } `; } diff --git a/src/seqta/ui/sidebar/Sidebar.svelte b/src/seqta/ui/sidebar/Sidebar.svelte index bd91bf9a..ca6cdb79 100644 --- a/src/seqta/ui/sidebar/Sidebar.svelte +++ b/src/seqta/ui/sidebar/Sidebar.svelte @@ -34,33 +34,17 @@ function closeEdit() { sidebarState.setEditMode(false); - menuEl.classList.remove("bsplus-sidebar-edit-mode"); void import("@/seqta/utils/Openers/menuOptionsState").then((mod) => { mod.setMenuOptionsOpen(false); }); } - function findFolder( - items: SidebarItemModel[], - key: string, - ): SidebarItemModel | null { - for (const item of items) { - if (item.key === key) return item; - if (item.children.length) { - const nested = findFolder(item.children, key); - if (nested) return nested; - } - } - return null; - } - - /** Folder meta for each drill frame (icon/label from the live tree). */ - const drillFolders = $derived.by(() => { - return sidebarState.drillStack.map((frame) => { - const folder = findFolder(sidebarState.items, frame.key); - return { frame, folder }; - }); - }); + const drillFolders = $derived( + sidebarState.drillStack.map((frame) => ({ + frame, + folder: sidebarState.findByKey(frame.key), + })), + ); function onToggleVisible(key: string, visible: boolean) { sidebarState.setItemVisibility(key, visible); @@ -105,26 +89,13 @@ }; }); - // SEQTA strips `.active` from `#menu li` after clicks — keep it on our custom - // rows so theme animations (palm/sand) and drill `.sub` chrome stay applied. + // SEQTA strips `.active` from `#menu li` after clicks — re-apply from state. + // Do NOT MutationObserver class changes here: restore writes `.active`, SEQTA + // strips it again, and the feedback loop freezes the tab. $effect(() => { - // Re-run when the route-active key changes so we re-bind the observer target. void sidebarState.activeKey; void sidebarState.isDrilling; - - const root = document.getElementById("bsplus-sidebar-root"); - if (!root) return; - restoreCustomMenuActive(); - - const observer = new MutationObserver(() => restoreCustomMenuActive()); - observer.observe(root, { - subtree: true, - attributes: true, - attributeFilter: ["class"], - }); - - return () => observer.disconnect(); }); @@ -151,9 +122,8 @@ {item} active={sidebarState.activeKey === item.key} compact={sidebarState.compact} - editMode={sidebarState.editMode} + editMode={true} visible={itemVisible(item.key)} - drillEnter={false} {onActivate} {onToggleVisible} {onDragStart} @@ -173,21 +143,19 @@ {:else if drillFolders.length} - + {#each sidebarState.visibleRootItems as item (item.key)} {#if item.key === drillFolders[0].frame.key} {@render drillLevel(drillFolders, 0)} {:else} {/if} @@ -198,13 +166,9 @@ {item} active={sidebarState.activeKey === item.key} compact={sidebarState.compact} - editMode={sidebarState.editMode} - visible={itemVisible(item.key)} - drillEnter={false} + editMode={false} + visible={true} {onActivate} - {onToggleVisible} - {onDragStart} - {onDrop} /> {/each} {/if} @@ -232,7 +196,15 @@ {/if} {current.frame.label} -
+
{ + if (e.target === e.currentTarget) { + sidebarState.clearEnterFrame(current.frame.key); + } + }} + >