fix: stabilize custom sidebar active state

This commit is contained in:
SethBurkart123
2026-08-27 09:06:08 +10:00
parent 8ccddc1ae0
commit 9f80ba11f1
8 changed files with 112 additions and 137 deletions
+23 -15
View File
@@ -704,20 +704,23 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
/* 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;
> #bsplus-sidebar-root .bsplus-sidebar-item {
width: auto !important;
max-width: none !important;
margin: 0 6px 8px !important;
box-sizing: border-box !important;
}
&:has(> #bsplus-sidebar-root > li.hasChildren.active > .sub)
> #bsplus-sidebar-root > li.item {
pointer-events: auto !important;
}
&:has(> #bsplus-sidebar-root > li.hasChildren.bsplus-active > .sub)
> #bsplus-sidebar-root
> li:not(.hasChildren.active),
&:has(> #bsplus-sidebar-root > li.hasChildren.active > .sub)
> li:not(.hasChildren.bsplus-active),
&:has(> #bsplus-sidebar-root > li.hasChildren.bsplus-active > .sub)
> #bsplus-sidebar-root
> li:not(.hasChildren.active)
> li:not(.hasChildren.bsplus-active)
* {
pointer-events: none !important;
}
@@ -733,17 +736,21 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
pointer-events: none;
}
> #bsplus-sidebar-root li.hasChildren.active > .sub {
> #bsplus-sidebar-root li.hasChildren.bsplus-active > .sub {
pointer-events: auto;
transform: none;
background: transparent !important;
background-image: none !important;
}
> #bsplus-sidebar-root li.hasChildren.active > .sub:has(.hasChildren.active) {
> #bsplus-sidebar-root li.hasChildren.bsplus-active
> .sub:has(.hasChildren.bsplus-active) {
pointer-events: none !important;
}
> #bsplus-sidebar-root li.hasChildren.active .hasChildren.active > .sub {
> #bsplus-sidebar-root li.hasChildren.bsplus-active
.hasChildren.bsplus-active
> .sub {
pointer-events: auto !important;
background: transparent !important;
background-image: none !important;
@@ -765,17 +772,17 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
background-image: none !important;
}
> #bsplus-sidebar-root > li.hasChildren.active > label,
> #bsplus-sidebar-root > li.hasChildren.bsplus-active > label,
> #bsplus-sidebar-root.drilling > li.hasChildren > 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 {
> #bsplus-sidebar-root:has(> li.hasChildren.bsplus-active > .sub)
> li:not(.hasChildren.bsplus-active)::before,
> #bsplus-sidebar-root:has(> li.hasChildren.bsplus-active > .sub)
> li:not(.hasChildren.bsplus-active)::after {
content: none !important;
display: none !important;
animation: none !important;
@@ -812,8 +819,9 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
#bsplus-sidebar-root .sub > ul {
list-style: none;
margin: 0;
padding: 0 0 16px;
padding: 8px 0 16px;
}
}
#menu li > label > svg,
+49 -20
View File
@@ -1,20 +1,22 @@
#menu ul:has(li.hasChildren.active) li.hasChildren.active {
#menu ul:has(li.hasChildren:is(.active, .bsplus-active))
li.hasChildren:is(.active, .bsplus-active) {
box-shadow: inset 0px 0 var(--item-colour, transparent) !important;
background: transparent !important;
}
#menu ul:not(:has(li.hasChildren.active)) {
#menu ul:not(:has(li.hasChildren:is(.active, .bsplus-active))) {
overflow-y: scroll !important;
}
#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 {
#menu > ul:has(li.hasChildren:is(.active, .bsplus-active)) > li::before,
#menu > ul:has(li.hasChildren:is(.active, .bsplus-active)) > li::after,
#menu > ul ul:has(li.hasChildren:is(.active, .bsplus-active)) > li::before,
#menu > ul ul:has(li.hasChildren:is(.active, .bsplus-active)) > li::after,
#menu > ul:has(li.hasChildren:is(.active, .bsplus-active)) > li > label,
#menu > ul:has(li.hasChildren:is(.active, .bsplus-active)) > li > svg,
#menu > ul ul:has(li.hasChildren:is(.active, .bsplus-active)) > li > label,
#menu > ul ul:has(li.hasChildren:is(.active, .bsplus-active)) > li > svg {
/* !important beats theme animations (e.g. Beach palm-slide forwards). */
transform: translateX(-320px) !important;
animation: none !important;
}
@@ -28,12 +30,13 @@
transition: transform 0.3s ease, left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#menu > ul:has(li.hasChildren.active) > li.active {
#menu > ul:has(li.hasChildren:is(.active, .bsplus-active))
> li:is(.active, .bsplus-active) {
background: transparent !important;
}
#menu li.hasChildren:not(.active) li .sub {
display: none;
#menu li.hasChildren:not(:is(.active, .bsplus-active)) li .sub {
display: none; // improves performance
}
.sub .sub {
@@ -41,16 +44,42 @@
padding-top: 2px;
}
.sub:has(ul > li.hasChildren.active) > .nav > .back {
.sub:has(ul > li.hasChildren:is(.active, .bsplus-active)) > .nav > .back {
display: none !important;
}
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li::before,
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li::before,
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li > label,
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul:has(li.hasChildren.active) > li > svg,
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li > label,
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(li.hasChildren.active) > li > svg {
/* Icon-only collapsed: submenu slides over narrow icons */
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul:has(li.hasChildren:is(.active, .bsplus-active))
> li::before,
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul
ul:has(li.hasChildren:is(.active, .bsplus-active))
> li::before,
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul:has(li.hasChildren:is(.active, .bsplus-active))
> li
> label,
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul:has(li.hasChildren:is(.active, .bsplus-active))
> li
> svg,
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul
ul:has(li.hasChildren:is(.active, .bsplus-active))
> li
> label,
body.icon-only-sidebar:not(:has(#menu li.hasChildren:is(.active, .bsplus-active)))
#menu
> ul
ul:has(li.hasChildren:is(.active, .bsplus-active))
> li
> svg {
transform: translateX(-70px);
}
+6 -4
View File
@@ -19,9 +19,9 @@ $sidebar-motion: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#menu.bsplus-custom-sidebar {
$item: "> #bsplus-sidebar-root > li.item:not(.bsplus-sidebar-edit-header):not(.bsplus-sidebar-edit-actions)";
$label: "> #bsplus-sidebar-root > li.item > label:not(.toggle)";
$active: "> #bsplus-sidebar-root > li.item.active:not(.hasChildren)";
$item: "> #bsplus-sidebar-root .bsplus-sidebar-item";
$label: "> #bsplus-sidebar-root .bsplus-sidebar-item > label:not(.toggle)";
$active: "> #bsplus-sidebar-root .bsplus-sidebar-item.bsplus-active:not(.hasChildren)";
/* Look: base radius (pill / sharp / strip override).
Density/radius animate with the same easing as sidebar width. */
@@ -37,6 +37,7 @@ $sidebar-motion: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#{$label} {
min-height: 28px !important;
transition:
font-size $sidebar-motion,
line-height $sidebar-motion,
@@ -147,7 +148,7 @@ $sidebar-motion: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
margin: 0 !important;
font-size: 13px !important;
line-height: 1.2 !important;
min-height: 0 !important;
min-height: 20px !important;
height: auto !important;
white-space: nowrap !important;
@@ -169,6 +170,7 @@ $sidebar-motion: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0 !important;
font-size: 17px !important;
line-height: 1.25 !important;
min-height: 30px !important;
> svg {
width: 30px !important;
@@ -1,3 +1,4 @@
import { getNativeMenuList } from "@/seqta/ui/sidebar/parseNativeMenu";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { renderAnalyticsPage } from "./ui";
@@ -23,10 +24,11 @@ export async function loadAnalyticsPage(): Promise<void> {
async function loadAnalyticsPageInner(): Promise<void> {
document.title = "Analytics ― SEQTA Learn";
document.querySelectorAll("#menu .item").forEach((item) => {
const menu = getNativeMenuList();
menu?.querySelectorAll(".item").forEach((item) => {
item.classList.remove("active");
});
document.querySelector('[data-key="analytics"]')?.classList.add("active");
menu?.querySelector('[data-key="analytics"]')?.classList.add("active");
let main: HTMLElement;
try {
+4 -16
View File
@@ -2,10 +2,7 @@
import { onDestroy } from "svelte";
import Sortable from "sortablejs";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
restoreCustomMenuActive,
sidebarState,
} from "./sidebarState.svelte";
import { sidebarState } from "./sidebarState.svelte";
import SidebarItem from "./SidebarItem.svelte";
import type {
SidebarDrillFrame,
@@ -102,7 +99,7 @@
`${Math.round(width)}px`,
);
// Fallback clone is created just after onStart; pin size + drop
// `.active` so theme/active rules don't expand it to full width.
// Active styling makes fallback clones wider; keep the source width.
requestAnimationFrame(() => {
const dragEl = document.querySelector(
".bsplus-sortable-drag",
@@ -110,7 +107,7 @@
if (!dragEl) return;
dragEl.style.width = `${Math.round(width)}px`;
dragEl.style.maxWidth = `${Math.round(width)}px`;
dragEl.classList.remove("active");
dragEl.classList.remove("bsplus-active");
});
},
onEnd: (evt) => {
@@ -156,15 +153,6 @@
};
});
// 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(() => {
void sidebarState.activeKey;
void sidebarState.isDrilling;
restoreCustomMenuActive();
});
// Drill `.sub` is position:absolute inside this scrollport — if the list was
// scrolled down (e.g. Folios/Goals near the bottom), the panel sits under the
// logo until we reset. Also reset when going back up the stack.
@@ -269,7 +257,7 @@
{@const folder = current.folder}
<li
class="item hasChildren bsplus-sidebar-item"
class:active={true}
class:bsplus-active={true}
data-key={current.frame.key}
data-path={folder?.path ?? undefined}
style:--item-colour={folder?.itemColour || undefined}
+12 -14
View File
@@ -29,17 +29,18 @@
const GRIP_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="5" cy="3" r="1.35" fill="currentColor"/><circle cx="11" cy="3" r="1.35" fill="currentColor"/><circle cx="5" cy="8" r="1.35" fill="currentColor"/><circle cx="11" cy="8" r="1.35" fill="currentColor"/><circle cx="5" cy="13" r="1.35" fill="currentColor"/><circle cx="11" cy="13" r="1.35" fill="currentColor"/></svg>`;
</script>
<!-- SEQTA class names (item / hasChildren / active) so theme CSS keeps matching. -->
<!-- Keep SEQTA's structural attributes so existing themes recognize each row. -->
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
<li
class="item bsplus-sidebar-item"
class:active={active}
class:bsplus-active={active}
class:hasChildren={item.hasChildren}
class:edit-mode={editMode}
class:hidden-item={editMode && !visible}
class:compact={compact}
class:draggable={editMode}
style:--item-colour={item.itemColour || undefined}
data-colour={item.itemColour || undefined}
data-key={item.key}
data-path={item.path ?? undefined}
data-betterseqta={item.betterseqta ? "true" : undefined}
@@ -131,7 +132,7 @@
color: inherit;
}
.bsplus-sidebar-item:hover:not(.active) {
.bsplus-sidebar-item:hover:not(.bsplus-active) {
background: rgba(0, 0, 0, 0.15);
}
@@ -140,8 +141,8 @@
box-shadow: 0 0 0 2px var(--theme-primary, #fff);
}
.bsplus-sidebar-item.active:not(.hasChildren),
.bsplus-sidebar-item.active:not(.hasChildren):hover {
.bsplus-sidebar-item.bsplus-active:not(.hasChildren),
.bsplus-sidebar-item.bsplus-active:not(.hasChildren):hover {
background: rgba(0, 0, 0, 0.35);
color: #fff;
}
@@ -250,18 +251,15 @@
.colour-bar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 3px;
border-radius: 8px 0 0 8px;
background: var(--item-colour, transparent);
transition: width 100ms ease;
inset: 0;
border-radius: inherit;
box-shadow: inset 3px 0 var(--item-colour, transparent);
transition: box-shadow 100ms ease;
pointer-events: none;
}
.bsplus-sidebar-item:hover .colour-bar,
.bsplus-sidebar-item.active .colour-bar {
width: 6px;
.bsplus-sidebar-item.bsplus-active .colour-bar {
box-shadow: inset 6px 0 var(--item-colour, transparent);
}
</style>
+4 -64
View File
@@ -37,12 +37,6 @@ function filterVisible(items: SidebarItem[]): SidebarItem[] {
return items.filter((item) => menuItems[item.key]?.toggle !== false);
}
function ensureActive(el: Element | null | undefined) {
if (el instanceof HTMLElement && !el.classList.contains("active")) {
el.classList.add("active");
}
}
function resetSidebarScroll() {
const root = document.getElementById("bsplus-sidebar-root");
if (!(root instanceof HTMLElement)) return;
@@ -52,52 +46,6 @@ function resetSidebarScroll() {
});
}
/**
* SEQTA (and some themes) strip `.active` from `#menu li` after navigation.
* Theme decorations and drill `.sub` chrome depend on that class on our list.
*
* While drilling, never re-apply route-active on root leaves — themes like Beach
* paint palm/sand on `#menu > ul > li:not(.hasChildren).active`, and our `.sub`
* is transparent so those decorations show through over folder contents.
*/
export function restoreCustomMenuActive() {
const root = document.getElementById("bsplus-sidebar-root");
if (!root) return;
for (const li of root.querySelectorAll("li.hasChildren")) {
if (!(li instanceof HTMLElement)) continue;
if (!li.querySelector(":scope > .sub")) continue;
ensureActive(li);
}
const activeKey = sidebarState.activeKey;
const drilling = sidebarState.isDrilling;
if (drilling) {
if (activeKey) {
ensureActive(
root.querySelector(`.sub li.item[data-key="${CSS.escape(activeKey)}"]`),
);
}
for (const li of root.querySelectorAll(
'.sub li.item[aria-current="page"]',
)) {
ensureActive(li);
}
return;
}
if (activeKey) {
ensureActive(
root.querySelector(`li.item[data-key="${CSS.escape(activeKey)}"]`),
);
}
for (const li of root.querySelectorAll('li.item[aria-current="page"]')) {
ensureActive(li);
}
}
/** Clear native drill state so it cannot steal pointer-events from the custom list. */
export function clearNativeDrillActive(menu: HTMLElement) {
getNativeMenuList(menu)
@@ -251,7 +199,10 @@ class SidebarState {
for (const frame of this.drillStack) {
const folder = cursor.find((item) => item.key === frame.key);
if (!folder?.hasChildren) break;
if (!folder?.hasChildren) {
next.push(...this.drillStack.slice(next.length));
break;
}
const children = filterVisible(folder.children);
next.push({ key: folder.key, label: folder.label, items: children });
cursor = children;
@@ -380,18 +331,7 @@ class SidebarState {
const native = findNativeMenuEntry(menu, item);
if (native) {
// Clear native drill once only — repeating clearNativeDrillActive fights
// SEQTA (it re-adds .active) and used to freeze the tab via menu sync.
clearNativeDrillActive(menu);
native.click();
clearNativeDrillActive(menu);
restoreCustomMenuActive();
requestAnimationFrame(() => {
clearNativeDrillActive(menu);
restoreCustomMenuActive();
});
// Later pass restores custom `.active` only (no native clear loop).
setTimeout(() => restoreCustomMenuActive(), 50);
return;
}
+10 -2
View File
@@ -55,6 +55,10 @@ function sendPayload(payload: Record<string, unknown>): void {
bridge.setAttribute("data-rev", String(Number(bridge.getAttribute("data-rev") || "0") + 1));
}
function preserveCustomSidebarActive(css: string): string {
return css.replace(/\.active(?![\w-])/g, ":is(.active, .bsplus-active)");
}
export async function syncThemeToPage(input: ThemePageSyncInput): Promise<void> {
if (input.clear) {
sendPayload({ clear: true });
@@ -63,8 +67,12 @@ export async function syncThemeToPage(input: ThemePageSyncInput): Promise<void>
const payload: Record<string, unknown> = {};
if (input.clearPreview) payload.clearPreview = true;
if (input.customCss !== undefined) payload.customCss = input.customCss;
if (input.previewCss !== undefined) payload.previewCss = input.previewCss;
if (input.customCss !== undefined) {
payload.customCss = preserveCustomSidebarActive(input.customCss);
}
if (input.previewCss !== undefined) {
payload.previewCss = preserveCustomSidebarActive(input.previewCss);
}
if (input.images !== undefined) {
payload.images = await Promise.all(
input.images.map(async (image) => ({