feat: minor fixes before update

This commit is contained in:
2026-07-24 09:32:00 +09:30
parent f926127687
commit b5347e2732
16 changed files with 454 additions and 81 deletions
+30 -6
View File
@@ -97,6 +97,20 @@
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.
$effect(() => {
void sidebarState.drillStack.length;
void sidebarState.enterFrameKey;
const root = document.getElementById("bsplus-sidebar-root");
if (!root) return;
root.scrollTop = 0;
requestAnimationFrame(() => {
root.scrollTop = 0;
});
});
</script>
<!--
@@ -212,10 +226,15 @@
role="button"
tabindex="0"
aria-label="Back"
onclick={() => sidebarState.goBack()}
onclick={(e) => {
e.preventDefault();
e.stopPropagation();
sidebarState.goBack();
}}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
sidebarState.goBack();
}
}}
@@ -303,22 +322,27 @@
.bsplus-sidebar-edit-actions {
display: flex;
gap: 8px;
width: 85%;
margin: 12px auto 16px;
width: calc(100% - 12px);
margin: 12px 6px 16px;
padding: 0;
list-style: none;
cursor: default;
box-sizing: border-box;
}
.edit-btn {
flex: 1;
padding: 10px 12px;
flex: 1 1 0;
min-width: 0;
padding: 10px 8px;
border: none;
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
color: inherit;
font: inherit;
font-family: inherit;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
cursor: pointer;
transition:
background-color 0.2s ease,
+36 -10
View File
@@ -56,13 +56,18 @@
ondragstart={() => onDragStart?.(item.key)}
ondragover={(e) => e.preventDefault()}
ondrop={() => onDrop?.(item.key)}
onclick={() => {
onclick={(e) => {
// Keep SEQTA's #menu handlers from seeing custom-list clicks — that fights
// our drill UI and can freeze the tab (Goals / Folios / etc.).
e.preventDefault();
e.stopPropagation();
if (!editMode) onActivate(item);
}}
onkeydown={(e) => {
if (editMode) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
onActivate(item);
}
}}
@@ -118,10 +123,11 @@
user-select: none;
}
.bsplus-sidebar-item > label {
.bsplus-sidebar-item > label:not(.toggle) {
display: flex;
align-items: center;
width: 100%;
flex: 1 1 auto;
width: auto;
min-width: 0;
margin: 0;
padding: 12px;
@@ -129,7 +135,7 @@
color: inherit;
}
.bsplus-sidebar-item:hover {
.bsplus-sidebar-item:hover:not(.active) {
background: rgba(0, 0, 0, 0.15);
}
@@ -138,7 +144,8 @@
box-shadow: 0 0 0 2px var(--theme-primary, #fff);
}
.bsplus-sidebar-item.active:not(.hasChildren) {
.bsplus-sidebar-item.active:not(.hasChildren),
.bsplus-sidebar-item.active:not(.hasChildren):hover {
background: rgba(0, 0, 0, 0.35);
color: #fff;
}
@@ -147,7 +154,7 @@
justify-content: center;
}
.bsplus-sidebar-item.compact > label {
.bsplus-sidebar-item.compact > label:not(.toggle) {
padding: 8px;
justify-content: center;
}
@@ -158,6 +165,18 @@
.bsplus-sidebar-item.edit-mode {
cursor: grab;
width: 100%;
max-width: 100%;
gap: 4px;
padding-right: 4px;
box-sizing: border-box;
}
.bsplus-sidebar-item.edit-mode > label:not(.toggle) {
flex: 1 1 0;
width: 0;
min-width: 0;
padding-right: 4px;
}
.bsplus-sidebar-item :global(label > svg) {
@@ -171,8 +190,9 @@
.label {
flex: 1;
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
@@ -194,17 +214,23 @@
}
.toggle {
margin-right: 12px;
margin: 0 10px 0 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
flex: 0 0 auto;
width: auto;
padding: 0;
position: relative;
z-index: 2;
}
.toggle input {
width: 18px;
height: 18px;
margin: 0;
flex-shrink: 0;
accent-color: var(--theme-primary, #fff);
}
+56 -4
View File
@@ -21,6 +21,7 @@ let menuEl: HTMLElement | null = null;
let menuObserver: MutationObserver | null = null;
let syncTimer: ReturnType<typeof setTimeout> | null = null;
let hashListenerAttached = false;
let sidebarCaptureAttached = false;
let earlyPrepareStarted = false;
let catchupTimer: ReturnType<typeof setInterval> | null = null;
let nativeMenuListenerAttached = false;
@@ -56,6 +57,44 @@ function scheduleSync() {
}, 50);
}
/**
* Capture-phase: own all clicks inside the custom list so SEQTA's #menu handlers
* never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab.
*/
function onCustomSidebarCaptureClick(event: MouseEvent) {
if (!menuEl || sidebarState.editMode) return;
const root = document.getElementById(ROOT_ID);
const target = event.target;
if (!(target instanceof Element) || !root?.contains(target)) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const back = target.closest(".back");
if (back instanceof HTMLElement && root.contains(back)) {
sidebarState.goBack();
return;
}
const li = target.closest("li.item[data-key]");
if (!(li instanceof HTMLElement) || !root.contains(li)) return;
// Already-open folder chrome (renders its own `.sub`) — ignore; use Back.
if (
li.classList.contains("hasChildren") &&
li.querySelector(":scope > .sub")
) {
return;
}
const key = li.dataset.key;
if (!key) return;
const item = sidebarState.findByKey(key);
if (item) sidebarState.activateItem(item, menuEl);
}
function onHashChange() {
sidebarState.syncActiveFromLocation();
if (menuEl) clearNativeDrillActive(menuEl);
@@ -189,11 +228,14 @@ export async function mountCustomSidebar(): Promise<boolean> {
menuObserver?.disconnect();
menuObserver = new MutationObserver((mutations) => {
const ours = document.getElementById(ROOT_ID);
// Ignore our list entirely. Also ignore native `class` toggles — SEQTA
// re-adds drill `.active` after we clear it; syncing on that freezes the tab.
// Ignore our list entirely. Ignore native class/style churn — SEQTA and
// theme transitions rewrite those constantly; syncing on them freezes the tab.
const relevant = mutations.some((m) => {
if (ours?.contains(m.target as Node)) return false;
if (m.type === "attributes" && m.attributeName === "class") return false;
if (m.type === "attributes") {
const attr = m.attributeName;
if (attr === "class" || attr === "style") return false;
}
return true;
});
if (!relevant) return;
@@ -203,9 +245,14 @@ export async function mountCustomSidebar(): Promise<boolean> {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["style", "data-key", "data-path", "data-colour"],
attributeFilter: ["data-key", "data-path", "data-colour"],
});
if (!sidebarCaptureAttached) {
document.addEventListener("click", onCustomSidebarCaptureClick, true);
sidebarCaptureAttached = true;
}
if (!hashListenerAttached) {
window.addEventListener("hashchange", onHashChange);
hashListenerAttached = true;
@@ -242,6 +289,11 @@ export function unmountCustomSidebar() {
hashListenerAttached = false;
}
if (sidebarCaptureAttached) {
document.removeEventListener("click", onCustomSidebarCaptureClick, true);
sidebarCaptureAttached = false;
}
if (nativeMenuListenerAttached) {
window.removeEventListener("bsplus-native-menu-updated", syncFromMenu);
nativeMenuListenerAttached = false;
+33 -10
View File
@@ -43,6 +43,15 @@ function ensureActive(el: Element | null | undefined) {
}
}
function resetSidebarScroll() {
const root = document.getElementById("bsplus-sidebar-root");
if (!(root instanceof HTMLElement)) return;
root.scrollTop = 0;
requestAnimationFrame(() => {
root.scrollTop = 0;
});
}
/**
* SEQTA (and some themes) strip `.active` from `#menu li` after navigation.
* Theme decorations and drill `.sub` chrome depend on that class on our list.
@@ -247,17 +256,29 @@ class SidebarState {
}
}
openFolder(item: SidebarItem) {
openFolder(item: SidebarItem, menu?: HTMLElement) {
if (!item.hasChildren) return;
// Ignore duplicate opens (double-firing click / label + li).
if (this.drillStack.at(-1)?.key === item.key) return;
const frame: SidebarDrillFrame = {
key: item.key,
label: item.label,
items: filterVisible(item.children),
};
const isRoot = this.visibleRootItems.some((entry) => entry.key === item.key);
this.enterFrameKey = item.key;
this.drillStack = [
...this.drillStack,
{
key: item.key,
label: item.label,
items: filterVisible(item.children),
},
];
// Root folders replace the stack; nested folders append.
this.drillStack = isRoot ? [frame] : [...this.drillStack, frame];
// Keep native drill closed so SEQTA CSS :has(> ul > li.hasChildren.active)
// does not lock pointer-events on the custom list.
if (menu) clearNativeDrillActive(menu);
// Absolute `.sub` panels live inside the scrollport — jump to top so the
// drilled page isn't left under the logo when the list was scrolled down.
resetSidebarScroll();
}
clearEnterFrame(key?: string) {
@@ -270,11 +291,13 @@ class SidebarState {
if (!this.drillStack.length) return;
this.enterFrameKey = null;
this.drillStack = this.drillStack.slice(0, -1);
resetSidebarScroll();
}
resetDrill() {
this.enterFrameKey = null;
this.drillStack = [];
resetSidebarScroll();
}
setEditMode(enabled: boolean) {
@@ -320,7 +343,7 @@ class SidebarState {
if (this.editMode) return;
if (item.hasChildren) {
this.openFolder(item);
this.openFolder(item, menu);
return;
}