diff --git a/src/css/injected.scss b/src/css/injected.scss index bbace617..15b0e9c8 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -722,6 +722,13 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root), 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 { pointer-events: none; } @@ -742,13 +749,24 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root), 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); + > #bsplus-sidebar-root.drilling li.hasChildren > .sub:not(:has(.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 > .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; } @@ -764,6 +782,14 @@ html.bsplus-custom-title-pending #title > :not(#bsplus-title-root), 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 { display: flex; 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 section > label > svg { margin: 0 10px 0 4px; diff --git a/src/css/injected/sidebar-animation.scss b/src/css/injected/sidebar-animation.scss index 5718924c..03cd1172 100644 --- a/src/css/injected/sidebar-animation.scss +++ b/src/css/injected/sidebar-animation.scss @@ -15,44 +15,25 @@ #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 { - /* !important beats theme animations (e.g. Beach palm-slide forwards). */ transform: translateX(-320px) !important; animation: none !important; } -/* - * Custom sidebar: park sibling chrome while `.drilling` is set. - * SEQTA strips/restores `.active` on every nested leaf click; keying only off - * `: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 ul > li > label, +#menu ul > li > svg { + transition: transform 0.3s ease; } -#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li::before, -#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li::after, -#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 .sub { + transition: transform 0.3s ease, left 0.4s cubic-bezier(0.4, 0, 0.2, 1); } -#menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling { - overflow: hidden !important; +#menu > ul:has(li.hasChildren.active) > li.active { + background: transparent !important; } #menu li.hasChildren:not(.active) li .sub { - display: none; // improves performance + display: none; } .sub .sub { @@ -64,27 +45,6 @@ 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 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, @@ -94,12 +54,92 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) #menu > ul ul:has( 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, -body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling > li > svg, -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, -body.icon-only-sidebar #menu.bsplus-custom-sidebar #bsplus-sidebar-root.drilling .sub:has(.sub) > ul > li > svg { - transform: translateX(-70px) !important; - transition: none !important; +/* + * Custom sidebar drill slides — use `.drilling` / `.drill-entering` / `.drill-returning`, + * not `:has(.active)` (SEQTA strips `.active` on nested clicks). + */ +#menu.bsplus-custom-sidebar #bsplus-sidebar-root { + --bsplus-slide-off: -320px; + + .sub { + 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); + } } diff --git a/src/seqta/ui/sidebar/Sidebar.svelte b/src/seqta/ui/sidebar/Sidebar.svelte index c02438e1..5c43a338 100644 --- a/src/seqta/ui/sidebar/Sidebar.svelte +++ b/src/seqta/ui/sidebar/Sidebar.svelte @@ -195,6 +195,8 @@ class="logo-link bsplus-sidebar-list" class:noscroll={sidebarState.isDrilling} class:drilling={sidebarState.isDrilling} + class:drill-entering={sidebarState.enterFrameKey != null} + class:drill-returning={sidebarState.drillReturning} class:compact={sidebarState.compact} class:edit-mode={sidebarState.editMode} class:is-sorting={dragging} @@ -283,7 +285,8 @@ class="sub" class:bsplus-sub-enter={sidebarState.enterFrameKey === current.frame.key} onanimationend={(e) => { - if (e.target === e.currentTarget) { + if (e.target !== e.currentTarget) return; + if (sidebarState.enterFrameKey === current.frame.key) { sidebarState.clearEnterFrame(current.frame.key); } }} diff --git a/src/seqta/ui/sidebar/mountCustomSidebar.ts b/src/seqta/ui/sidebar/mountCustomSidebar.ts index 9fbf31d5..e2f846f8 100644 --- a/src/seqta/ui/sidebar/mountCustomSidebar.ts +++ b/src/seqta/ui/sidebar/mountCustomSidebar.ts @@ -101,7 +101,7 @@ function scheduleSync() { * never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab. */ function onCustomSidebarCaptureClick(event: MouseEvent) { - if (!menuEl || sidebarState.editMode) return; + if (!menuEl || sidebarState.editMode || sidebarState.drillReturning) return; const root = document.getElementById(ROOT_ID); const target = event.target; diff --git a/src/seqta/ui/sidebar/sidebarState.svelte.ts b/src/seqta/ui/sidebar/sidebarState.svelte.ts index 565170fb..543f8830 100644 --- a/src/seqta/ui/sidebar/sidebarState.svelte.ts +++ b/src/seqta/ui/sidebar/sidebarState.svelte.ts @@ -184,8 +184,9 @@ class SidebarState { activePath = $state(""); editMode = $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(null); + drillReturning = $state(false); visibleRootItems = $derived( filterVisible(orderItems(this.items, settingsState.menuorder ?? [])), @@ -262,10 +263,11 @@ class SidebarState { } openFolder(item: SidebarItem, menu?: HTMLElement) { - if (!item.hasChildren) return; - // Ignore duplicate opens (double-firing click / label + li). + if (!item.hasChildren || this.drillReturning) return; if (this.drillStack.at(-1)?.key === item.key) return; + this.drillReturning = false; + const frame: SidebarDrillFrame = { key: item.key, label: item.label, @@ -293,14 +295,23 @@ class SidebarState { } goBack() { - if (!this.drillStack.length) return; + if (!this.drillStack.length || this.drillReturning) return; + this.enterFrameKey = null; this.drillStack = this.drillStack.slice(0, -1); resetSidebarScroll(); + + if (settingsState.animations !== true) return; + + this.drillReturning = true; + window.setTimeout(() => { + this.drillReturning = false; + }, 360); } resetDrill() { this.enterFrameKey = null; + this.drillReturning = false; this.drillStack = []; resetSidebarScroll(); } @@ -350,7 +361,7 @@ class SidebarState { } activateItem(item: SidebarItem, menu: HTMLElement) { - if (this.editMode) return; + if (this.editMode || this.drillReturning) return; if (item.hasChildren) { this.openFolder(item, menu); @@ -360,6 +371,13 @@ class SidebarState { this.activeKey = item.key; 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); if (native) { // Clear native drill once only — repeating clearNativeDrillActive fights