fix: fix sidebar animation weirdness in sidebar folders

This commit is contained in:
2026-08-24 14:04:08 +09:30
parent a411d646ae
commit dde5096d1c
5 changed files with 155 additions and 78 deletions
+31 -15
View File
@@ -722,6 +722,13 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
pointer-events: none !important; pointer-events: none !important;
} }
/* Same locks keyed off `.drilling` so nested leaf clicks do not flicker when
SEQTA briefly strips `.active` from folder rows. */
> #bsplus-sidebar-root.drilling > li:not(.hasChildren),
> #bsplus-sidebar-root.drilling > li:not(.hasChildren) * {
pointer-events: none !important;
}
> #bsplus-sidebar-root .sub { > #bsplus-sidebar-root .sub {
pointer-events: none; pointer-events: none;
} }
@@ -742,13 +749,24 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
background-image: none !important; background-image: none !important;
} }
/* One-shot enter — not tied to `.active` (SEQTA strips/restores that on every > #bsplus-sidebar-root.drilling li.hasChildren > .sub:not(:has(.sub)) {
nested click, which would restart the animation). */ pointer-events: auto !important;
> #bsplus-sidebar-root .sub.bsplus-sub-enter { background: transparent !important;
animation: bsplus-submenu-enter 0.32s cubic-bezier(0.22, 1, 0.36, 1); background-image: none !important;
} }
> #bsplus-sidebar-root > li.hasChildren.active > label { > #bsplus-sidebar-root.drilling li.hasChildren > .sub:has(.sub) {
pointer-events: none !important;
}
> #bsplus-sidebar-root.drilling li.hasChildren > .sub .sub {
pointer-events: auto !important;
background: transparent !important;
background-image: none !important;
}
> #bsplus-sidebar-root > li.hasChildren.active > label,
> #bsplus-sidebar-root.drilling > li.hasChildren > label {
pointer-events: none !important; pointer-events: none !important;
} }
@@ -764,6 +782,14 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
opacity: 0 !important; opacity: 0 !important;
} }
> #bsplus-sidebar-root.drilling > li:not(.hasChildren)::before,
> #bsplus-sidebar-root.drilling > li:not(.hasChildren)::after {
content: none !important;
display: none !important;
animation: none !important;
opacity: 0 !important;
}
#bsplus-sidebar-root .sub .back { #bsplus-sidebar-root .sub .back {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -790,16 +816,6 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root),
} }
} }
@keyframes bsplus-submenu-enter {
from {
opacity: 0.35;
transform: translateX(28px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
#menu li > label > svg, #menu li > label > svg,
#menu section > label > svg { #menu section > label > svg {
margin: 0 10px 0 4px; margin: 0 10px 0 4px;
+95 -55
View File
@@ -15,44 +15,25 @@
#menu > ul:has(li.hasChildren.active) > li > svg, #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 > label,
#menu > ul ul:has(li.hasChildren.active) > li > svg { #menu > ul ul:has(li.hasChildren.active) > li > svg {
/* !important beats theme animations (e.g. Beach palm-slide forwards). */
transform: translateX(-320px) !important; transform: translateX(-320px) !important;
animation: none !important; animation: none !important;
} }
/* #menu ul > li > label,
* Custom sidebar: park sibling chrome while `.drilling` is set. #menu ul > li > svg {
* SEQTA strips/restores `.active` on every nested leaf click; keying only off transition: transform 0.3s ease;
* `:has(.active)` briefly undoes the translate and the 0.3s transform
* transition makes that look like the open animation flashing again.
*
* Scope carefully — only root siblings (and nested-folder siblings via
* `.sub:has(.sub)`), never the visible leaf rows in the current panel.
*/
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li::before,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li::after,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > label,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > svg {
transform: translateX(-320px) !important;
animation: none !important;
transition: none !important;
} }
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li::before, #menu .sub {
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li::after, transition: transform 0.3s ease, left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > label,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > svg {
transform: translateX(-320px) !important;
animation: none !important;
transition: none !important;
} }
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling { #menu > ul:has(li.hasChildren.active) > li.active {
overflow: hidden !important; background: transparent !important;
} }
#menu li.hasChildren:not(.active) li .sub { #menu li.hasChildren:not(.active) li .sub {
display: none; // improves performance display: none;
} }
.sub .sub { .sub .sub {
@@ -64,27 +45,6 @@
display: none !important; display: none !important;
} }
#menu ul > li > label,
#menu ul > li > svg {
transition: transform 0.3s ease;
}
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > label,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > svg,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > label,
#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > svg {
transition: none !important;
}
#menu .sub {
transition: transform 0.3s ease, left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#menu > ul:has(li.hasChildren.active) > li.active {
background: transparent !important;
}
/* Icon-only collapsed: submenu slides over narrow icons */
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: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 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 > label,
@@ -94,12 +54,92 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has(
transform: translateX(-70px); transform: translateX(-70px);
} }
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li::before, /*
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > label, * Custom sidebar drill slides — use `.drilling` / `.drill-entering` / `.drill-returning`,
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > svg, * not `:has(.active)` (SEQTA strips `.active` on nested clicks).
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li::before, */
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > label, #menu.bsplus-custom-sidebar #bsplus-sidebar-root {
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > svg { --bsplus-slide-off: -320px;
transform: translateX(-70px) !important;
.sub {
transition: none !important; transition: none !important;
} }
&.drilling {
overflow: hidden !important;
&:not(.drill-returning) > li::before,
&:not(.drill-returning) > li::after,
&:not(.drill-returning) > li > label,
&:not(.drill-returning) > li > svg,
&:not(.drill-returning) .sub:has(.sub) > ul > li::before,
&:not(.drill-returning) .sub:has(.sub) > ul > li::after,
&:not(.drill-returning) .sub:has(.sub) > ul > li > label,
&:not(.drill-returning) .sub:has(.sub) > ul > li > svg {
transform: translateX(var(--bsplus-slide-off)) !important;
animation: none !important;
}
&:not(.drill-entering):not(.drill-returning) {
> li::before,
> li::after,
> li > label,
> li > svg,
.sub:has(.sub) > ul > li::before,
.sub:has(.sub) > ul > li::after,
.sub:has(.sub) > ul > li > label,
.sub:has(.sub) > ul > li > svg {
transition: none !important;
}
.sub {
transform: translateX(0) !important;
opacity: 1 !important;
}
}
.sub:has(.sub) > .nav > .back {
display: none !important;
}
}
&.drill-entering .sub.bsplus-sub-enter {
animation: bsplus-submenu-enter 0.32s cubic-bezier(0.22, 1, 0.36, 1);
}
&.drill-returning:not(.drilling) > li::before,
&.drill-returning:not(.drilling) > li::after,
&.drill-returning:not(.drilling) > li > label,
&.drill-returning:not(.drilling) > li > svg,
&.drilling.drill-returning > li.hasChildren > .sub > ul > li::before,
&.drilling.drill-returning > li.hasChildren > .sub > ul > li::after,
&.drilling.drill-returning > li.hasChildren > .sub > ul > li > label,
&.drilling.drill-returning > li.hasChildren > .sub > ul > li > svg {
transition: none !important;
animation: bsplus-sidebar-return 0.32s cubic-bezier(0.22, 1, 0.36, 1) both !important;
}
}
body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root {
--bsplus-slide-off: -70px;
}
@keyframes bsplus-sidebar-return {
from {
transform: translateX(var(--bsplus-slide-off, -320px));
}
to {
transform: translateX(0);
}
}
@keyframes bsplus-submenu-enter {
from {
opacity: 0.35;
transform: translateX(28px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
+4 -1
View File
@@ -195,6 +195,8 @@
class="logo-link bsplus-sidebar-list" class="logo-link bsplus-sidebar-list"
class:noscroll={sidebarState.isDrilling} class:noscroll={sidebarState.isDrilling}
class:drilling={sidebarState.isDrilling} class:drilling={sidebarState.isDrilling}
class:drill-entering={sidebarState.enterFrameKey != null}
class:drill-returning={sidebarState.drillReturning}
class:compact={sidebarState.compact} class:compact={sidebarState.compact}
class:edit-mode={sidebarState.editMode} class:edit-mode={sidebarState.editMode}
class:is-sorting={dragging} class:is-sorting={dragging}
@@ -283,7 +285,8 @@
class="sub" class="sub"
class:bsplus-sub-enter={sidebarState.enterFrameKey === current.frame.key} class:bsplus-sub-enter={sidebarState.enterFrameKey === current.frame.key}
onanimationend={(e) => { onanimationend={(e) => {
if (e.target === e.currentTarget) { if (e.target !== e.currentTarget) return;
if (sidebarState.enterFrameKey === current.frame.key) {
sidebarState.clearEnterFrame(current.frame.key); sidebarState.clearEnterFrame(current.frame.key);
} }
}} }}
+1 -1
View File
@@ -101,7 +101,7 @@ function scheduleSync() {
* never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab. * never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab.
*/ */
function onCustomSidebarCaptureClick(event: MouseEvent) { function onCustomSidebarCaptureClick(event: MouseEvent) {
if (!menuEl || sidebarState.editMode) return; if (!menuEl || sidebarState.editMode || sidebarState.drillReturning) return;
const root = document.getElementById(ROOT_ID); const root = document.getElementById(ROOT_ID);
const target = event.target; const target = event.target;
+23 -5
View File
@@ -184,8 +184,9 @@ class SidebarState {
activePath = $state(""); activePath = $state("");
editMode = $state(false); editMode = $state(false);
iconOnly = $state(false); iconOnly = $state(false);
/** Frame key whose `.sub` should play the one-shot enter animation. */ /** Folder key playing the one-shot panel enter animation. */
enterFrameKey = $state<string | null>(null); enterFrameKey = $state<string | null>(null);
drillReturning = $state(false);
visibleRootItems = $derived( visibleRootItems = $derived(
filterVisible(orderItems(this.items, settingsState.menuorder ?? [])), filterVisible(orderItems(this.items, settingsState.menuorder ?? [])),
@@ -262,10 +263,11 @@ class SidebarState {
} }
openFolder(item: SidebarItem, menu?: HTMLElement) { openFolder(item: SidebarItem, menu?: HTMLElement) {
if (!item.hasChildren) return; if (!item.hasChildren || this.drillReturning) return;
// Ignore duplicate opens (double-firing click / label + li).
if (this.drillStack.at(-1)?.key === item.key) return; if (this.drillStack.at(-1)?.key === item.key) return;
this.drillReturning = false;
const frame: SidebarDrillFrame = { const frame: SidebarDrillFrame = {
key: item.key, key: item.key,
label: item.label, label: item.label,
@@ -293,14 +295,23 @@ class SidebarState {
} }
goBack() { goBack() {
if (!this.drillStack.length) return; if (!this.drillStack.length || this.drillReturning) return;
this.enterFrameKey = null; this.enterFrameKey = null;
this.drillStack = this.drillStack.slice(0, -1); this.drillStack = this.drillStack.slice(0, -1);
resetSidebarScroll(); resetSidebarScroll();
if (settingsState.animations !== true) return;
this.drillReturning = true;
window.setTimeout(() => {
this.drillReturning = false;
}, 360);
} }
resetDrill() { resetDrill() {
this.enterFrameKey = null; this.enterFrameKey = null;
this.drillReturning = false;
this.drillStack = []; this.drillStack = [];
resetSidebarScroll(); resetSidebarScroll();
} }
@@ -350,7 +361,7 @@ class SidebarState {
} }
activateItem(item: SidebarItem, menu: HTMLElement) { activateItem(item: SidebarItem, menu: HTMLElement) {
if (this.editMode) return; if (this.editMode || this.drillReturning) return;
if (item.hasChildren) { if (item.hasChildren) {
this.openFolder(item, menu); this.openFolder(item, menu);
@@ -360,6 +371,13 @@ class SidebarState {
this.activeKey = item.key; this.activeKey = item.key;
if (item.path) this.activePath = item.path; if (item.path) this.activePath = item.path;
// Already inside this folder — never replay the one-shot enter animation when
// SEQTA rewrites `.active` on the route change.
if (this.isDrilling) {
this.enterFrameKey = null;
restoreCustomMenuActive();
}
const native = findNativeMenuEntry(menu, item); const native = findNativeMenuEntry(menu, item);
if (native) { if (native) {
// Clear native drill once only — repeating clearNativeDrillActive fights // Clear native drill once only — repeating clearNativeDrillActive fights