diff --git a/frontend/app.js b/frontend/app.js index 0734f78..52b63b0 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -232,6 +232,30 @@ if (quickSelect) quickSelect.value = selectedContextVault; } + function scrollTreeItemIntoView(element, alignToTop) { + if (!element) return; + const scrollContainer = document.getElementById("vault-panel-content"); + if (!scrollContainer) return; + + const containerRect = scrollContainer.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const isAbove = elementRect.top < containerRect.top; + const isBelow = elementRect.bottom > containerRect.bottom; + + if (!isAbove && !isBelow && !alignToTop) return; + + const currentTop = scrollContainer.scrollTop; + const offsetTop = element.offsetTop; + const targetTop = alignToTop + ? Math.max(0, offsetTop - 8) + : Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height * 0.35)); + + scrollContainer.scrollTo({ + top: targetTop, + behavior: "smooth", + }); + } + async function refreshSidebarForContext() { const container = document.getElementById("vault-tree"); container.innerHTML = ""; @@ -267,7 +291,7 @@ if (childContainer && childContainer.classList.contains("collapsed")) { await toggleVault(vaultItem, vaultName, true); } - vaultItem.scrollIntoView({ block: "nearest" }); + scrollTreeItemIntoView(vaultItem, false); } async function refreshTagsForContext() { @@ -323,6 +347,8 @@ const childContainer = document.getElementById(`vault-children-${vaultName}`); if (!childContainer) return; + scrollTreeItemIntoView(itemEl, false); + const shouldExpand = forceExpand || childContainer.classList.contains("collapsed"); if (shouldExpand) { @@ -364,6 +390,7 @@ fragment.appendChild(subContainer); dirItem.addEventListener("click", async () => { + scrollTreeItemIntoView(dirItem, false); if (subContainer.classList.contains("collapsed")) { if (subContainer.children.length === 0) { await loadDirectory(vaultName, item.path, subContainer); @@ -387,6 +414,7 @@ document.createTextNode(` ${displayName}`), ]); fileItem.addEventListener("click", () => { + scrollTreeItemIntoView(fileItem, false); openFile(vaultName, item.path); closeMobileSidebar(); }); @@ -747,6 +775,7 @@ function setPanelExpanded(panelKey, expanded) { panelState[panelKey] = expanded; + const sidebar = document.getElementById("sidebar"); const toggle = document.getElementById(`${panelKey}-panel-toggle`); const content = document.getElementById(`${panelKey}-panel-content`); if (!toggle || !content) return; @@ -762,6 +791,10 @@ if (tagSection) tagSection.classList.toggle("collapsed-panel", !expanded); if (resizeHandle) resizeHandle.classList.toggle("hidden", !expanded); } + if (sidebar) { + sidebar.classList.toggle("vault-collapsed", !panelState.vault); + sidebar.classList.toggle("tag-collapsed", !panelState.tag); + } safeCreateIcons(); } diff --git a/frontend/style.css b/frontend/style.css index 2752eeb..bd0eb6d 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -314,6 +314,17 @@ a:hover { flex-shrink: 0; } +.sidebar.vault-collapsed .tag-cloud-section { + flex: 1 1 auto; + min-height: 0; + max-height: none; +} + +.sidebar.tag-collapsed .sidebar-tree { + flex: 1 1 auto; + min-height: 0; +} + /* --- Sidebar filter --- */ .sidebar-filter { padding: 10px 12px; @@ -347,9 +358,11 @@ a:hover { .sidebar-tree { flex: 1; - overflow-y: auto; + overflow: hidden; padding: 12px 0; min-height: 80px; + display: flex; + flex-direction: column; } .sidebar-tree::-webkit-scrollbar { @@ -396,7 +409,29 @@ a:hover { } .sidebar-panel-content.collapsed { - display: none; + max-height: 0 !important; + opacity: 0; + overflow: hidden; + pointer-events: none; +} + +.sidebar-panel-content { + min-height: 0; + overflow: hidden; + opacity: 1; + transition: max-height 340ms ease, opacity 220ms ease; +} + +#vault-panel-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + max-height: 1000px; + scroll-behavior: smooth; +} + +#tag-panel-content { + max-height: 320px; } .tree-item { @@ -446,6 +481,7 @@ a:hover { .tree-children { padding-left: 16px; + overflow: hidden; } .tree-children.collapsed { @@ -476,14 +512,18 @@ a:hover { height: 180px; min-height: 60px; max-height: 400px; - overflow-y: auto; + overflow: hidden; flex-shrink: 0; + display: flex; + flex-direction: column; + transition: height 340ms ease, min-height 340ms ease, max-height 340ms ease, flex 340ms ease; } .tag-cloud-section.collapsed-panel { height: auto; min-height: 0; max-height: none; overflow: hidden; + flex: 0 0 auto; } .tag-cloud-section::-webkit-scrollbar { width: 6px; @@ -510,6 +550,11 @@ a:hover { padding: 0 16px 12px; } +.tag-cloud-section:not(.collapsed-panel) #tag-panel-content { + overflow-y: auto; + flex: 1 1 auto; +} + .tag-item { display: inline-block; padding: 2px 8px; @@ -1281,6 +1326,14 @@ body.resizing-v { min-height: 80px; } + #vault-panel-content { + max-height: none; + } + + #tag-panel-content { + max-height: none; + } + .help-content { padding: 20px 16px 28px; }