Replace collapsible sidebar panels with tab-based navigation for vaults and tags with debounced filtering and dynamic placeholder text

This commit is contained in:
Bruno Charest 2026-03-22 22:42:35 -04:00
parent 6a782750de
commit 0d60dd8acc
3 changed files with 162 additions and 250 deletions

View File

@ -22,10 +22,8 @@
let sidebarFilterCaseSensitive = false; let sidebarFilterCaseSensitive = false;
let searchCaseSensitive = false; let searchCaseSensitive = false;
let _iconDebounceTimer = null; let _iconDebounceTimer = null;
const panelState = { let activeSidebarTab = "vaults";
vault: true, let filterDebounce = null;
tag: true,
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// File extension → Lucide icon mapping // File extension → Lucide icon mapping
@ -435,7 +433,7 @@
function scrollTreeItemIntoView(element, alignToTop) { function scrollTreeItemIntoView(element, alignToTop) {
if (!element) return; if (!element) return;
const scrollContainer = document.getElementById("vault-panel-content"); const scrollContainer = document.getElementById("sidebar-panel-vaults");
if (!scrollContainer) return; if (!scrollContainer) return;
const containerRect = scrollContainer.getBoundingClientRect(); const containerRect = scrollContainer.getBoundingClientRect();
@ -483,7 +481,7 @@
} }
async function focusVaultInSidebar(vaultName) { async function focusVaultInSidebar(vaultName) {
setPanelExpanded("vault", true); switchSidebarTab("vaults");
const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`); const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
if (!vaultItem) return; if (!vaultItem) return;
document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused")); document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
@ -567,7 +565,7 @@
} }
async function focusPathInSidebar(vaultName, targetPath, options) { async function focusPathInSidebar(vaultName, targetPath, options) {
setPanelExpanded("vault", true); switchSidebarTab("vaults");
const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`); const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
if (!vaultItem) return; if (!vaultItem) return;
@ -701,24 +699,31 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Sidebar filter // Sidebar filter
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function initSidebarFilter() { function initSidebarFilter() {
const input = document.getElementById("sidebar-filter-input"); const input = document.getElementById("sidebar-filter-input");
const caseBtn = document.getElementById("sidebar-filter-case-btn"); const caseBtn = document.getElementById("sidebar-filter-case-btn");
const clearBtn = document.getElementById("sidebar-filter-clear-btn"); const clearBtn = document.getElementById("sidebar-filter-clear-btn");
input.addEventListener("input", async () => { input.addEventListener("input", () => {
const hasText = input.value.length > 0; const hasText = input.value.length > 0;
clearBtn.style.display = hasText ? "flex" : "none"; clearBtn.style.display = hasText ? "flex" : "none";
clearTimeout(filterDebounce);
filterDebounce = setTimeout(async () => {
const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase(); const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
if (hasText) { if (hasText) {
if (activeSidebarTab === "vaults") {
await performTreeSearch(q); await performTreeSearch(q);
} else { } else {
await restoreSidebarTree();
}
filterTagCloud(q); filterTagCloud(q);
}
} else {
if (activeSidebarTab === "vaults") {
await restoreSidebarTree();
} else {
filterTagCloud("");
}
}
}, 220);
}); });
caseBtn.addEventListener("click", async () => { caseBtn.addEventListener("click", async () => {
@ -726,21 +731,27 @@
caseBtn.classList.toggle("active"); caseBtn.classList.toggle("active");
const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase(); const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
if (input.value.trim()) { if (input.value.trim()) {
if (activeSidebarTab === "vaults") {
await performTreeSearch(q); await performTreeSearch(q);
} } else {
filterTagCloud(q); filterTagCloud(q);
}
}
}); });
clearBtn.addEventListener("click", () => { clearBtn.addEventListener("click", async () => {
input.value = ""; input.value = "";
clearBtn.style.display = "none"; clearBtn.style.display = "none";
sidebarFilterCaseSensitive = false; sidebarFilterCaseSensitive = false;
caseBtn.classList.remove("active"); caseBtn.classList.remove("active");
restoreSidebarTree(); clearTimeout(filterDebounce);
if (activeSidebarTab === "vaults") {
await restoreSidebarTree();
} else {
filterTagCloud(""); filterTagCloud("");
}
}); });
// Initially hide clear button
clearBtn.style.display = "none"; clearBtn.style.display = "none";
} }
@ -1284,47 +1295,36 @@
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Collapsible panels and help modal // Sidebar tabs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function initCollapsiblePanels() { function initSidebarTabs() {
bindPanelToggle("vault", "vault-panel-toggle", "vault-panel-content"); document.querySelectorAll(".sidebar-tab").forEach((tab) => {
bindPanelToggle("tag", "tag-panel-toggle", "tag-panel-content"); tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
setPanelExpanded("vault", true);
setPanelExpanded("tag", true);
}
function bindPanelToggle(panelKey, toggleId, contentId) {
const toggle = document.getElementById(toggleId);
const content = document.getElementById(contentId);
if (!toggle || !content) return;
toggle.addEventListener("click", () => {
setPanelExpanded(panelKey, !panelState[panelKey]);
}); });
} }
function setPanelExpanded(panelKey, expanded) { function switchSidebarTab(tab) {
panelState[panelKey] = expanded; activeSidebarTab = tab;
const sidebar = document.getElementById("sidebar"); document.querySelectorAll(".sidebar-tab").forEach((btn) => {
const toggle = document.getElementById(`${panelKey}-panel-toggle`); const isActive = btn.dataset.tab === tab;
const content = document.getElementById(`${panelKey}-panel-content`); btn.classList.toggle("active", isActive);
if (!toggle || !content) return; btn.setAttribute("aria-selected", isActive ? "true" : "false");
toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); });
content.classList.toggle("collapsed", !expanded); document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
const iconEl = toggle.querySelector("[data-lucide]"); const isActive = panel.id === `sidebar-panel-${tab}`;
if (iconEl) { panel.classList.toggle("active", isActive);
iconEl.setAttribute("data-lucide", expanded ? "chevron-down" : "chevron-right"); });
const filterInput = document.getElementById("sidebar-filter-input");
if (filterInput) {
filterInput.placeholder = tab === "vaults" ? "Filtrer fichiers..." : "Filtrer tags...";
} }
if (panelKey === "tag") { const query = filterInput
const tagSection = document.getElementById("tag-cloud-section"); ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase())
const resizeHandle = document.getElementById("tag-resize-handle"); : "";
if (tagSection) tagSection.classList.toggle("collapsed-panel", !expanded); if (query) {
if (resizeHandle) resizeHandle.classList.toggle("hidden", !expanded); if (tab === "vaults") performTreeSearch(query);
else filterTagCloud(query);
} }
if (sidebar) {
sidebar.classList.toggle("vault-collapsed", !panelState.vault);
sidebar.classList.toggle("tag-collapsed", !panelState.tag);
}
safeCreateIcons();
} }
function initHelpModal() { function initHelpModal() {
@ -2017,12 +2017,11 @@
initSearch(); initSearch();
initMobile(); initMobile();
initVaultContext(); initVaultContext();
initCollapsiblePanels(); initSidebarTabs();
initHelpModal(); initHelpModal();
initConfigModal(); initConfigModal();
initSidebarFilter(); initSidebarFilter();
initSidebarResize(); initSidebarResize();
initTagResize();
initEditor(); initEditor();
try { try {

View File

@ -158,12 +158,12 @@
<!-- Sidebar --> <!-- Sidebar -->
<aside class="sidebar" id="sidebar" role="navigation" aria-label="Navigation des vaults"> <aside class="sidebar" id="sidebar" role="navigation" aria-label="Navigation des vaults">
<div class="sidebar-tree" id="sidebar-tree">
<!-- Sidebar filter --> <!-- Sidebar filter -->
<div class="sidebar-filter"> <div class="sidebar-filter">
<i data-lucide="filter" class="sidebar-filter-icon" style="width:14px;height:14px"></i> <i data-lucide="filter" class="sidebar-filter-icon" style="width:14px;height:14px"></i>
<div class="sidebar-filter-input-wrapper"> <div class="sidebar-filter-input-wrapper">
<input type="text" id="sidebar-filter-input" placeholder="Filtrer fichiers et tags..." autocomplete="off"> <input type="text" id="sidebar-filter-input" placeholder="Filtrer fichiers..." autocomplete="off">
<div class="sidebar-filter-actions"> <div class="sidebar-filter-actions">
<button class="sidebar-filter-btn" id="sidebar-filter-case-btn" type="button" title="Respecter la casse" aria-label="Respecter la casse"> <button class="sidebar-filter-btn" id="sidebar-filter-case-btn" type="button" title="Respecter la casse" aria-label="Respecter la casse">
<span>Aa</span> <span>Aa</span>
@ -175,6 +175,22 @@
</div> </div>
</div> </div>
<!-- Tab bar -->
<div class="sidebar-tabs" role="tablist" aria-label="Sections sidebar">
<button class="sidebar-tab active" id="sidebar-tab-vaults" role="tab"
aria-selected="true" aria-controls="sidebar-panel-vaults" data-tab="vaults">
<i data-lucide="folder-tree" style="width:13px;height:13px"></i>
<span>Vaults</span>
</button>
<button class="sidebar-tab" id="sidebar-tab-tags" role="tab"
aria-selected="false" aria-controls="sidebar-panel-tags" data-tab="tags">
<i data-lucide="tag" style="width:13px;height:13px"></i>
<span>Tags</span>
</button>
</div>
<!-- Vaults panel -->
<div class="sidebar-tab-panel active" id="sidebar-panel-vaults" role="tabpanel" aria-labelledby="sidebar-tab-vaults">
<div class="custom-dropdown sidebar-dropdown" id="vault-quick-select-dropdown"> <div class="custom-dropdown sidebar-dropdown" id="vault-quick-select-dropdown">
<button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false"> <button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
<span class="custom-dropdown-selected">Tous les vaults</span> <span class="custom-dropdown-selected">Tous les vaults</span>
@ -185,27 +201,14 @@
</ul> </ul>
<input type="hidden" id="vault-quick-select" value="all"> <input type="hidden" id="vault-quick-select" value="all">
</div> </div>
<button class="sidebar-panel-toggle" id="vault-panel-toggle" type="button" aria-expanded="true" aria-controls="vault-panel-content">
<span class="sidebar-section-title">Vaults</span>
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
</button>
<div class="sidebar-panel-content" id="vault-panel-content">
<div id="vault-tree" role="tree" aria-label="Arborescence des fichiers"></div> <div id="vault-tree" role="tree" aria-label="Arborescence des fichiers"></div>
</div> </div>
</div>
<!-- Tag resize handle --> <!-- Tags panel -->
<div class="tag-resize-handle" id="tag-resize-handle"></div> <div class="sidebar-tab-panel" id="sidebar-panel-tags" role="tabpanel" aria-labelledby="sidebar-tab-tags">
<div class="tag-cloud-section" id="tag-cloud-section">
<button class="sidebar-panel-toggle" id="tag-panel-toggle" type="button" aria-expanded="true" aria-controls="tag-panel-content">
<span class="tag-cloud-title">Tags</span>
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
</button>
<div class="sidebar-panel-content" id="tag-panel-content">
<div class="tag-cloud" id="tag-cloud"></div> <div class="tag-cloud" id="tag-cloud"></div>
</div> </div>
</div>
</aside> </aside>
<!-- Sidebar resize handle --> <!-- Sidebar resize handle -->

View File

@ -548,29 +548,6 @@ select {
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar.vault-collapsed .tag-cloud-section {
flex: 1 1 auto;
min-height: 0;
max-height: none;
height: auto;
padding-top: 0;
}
.sidebar.vault-collapsed .sidebar-tree {
flex: 0 0 auto;
min-height: 0;
padding-bottom: 0;
}
.sidebar.vault-collapsed .tag-resize-handle {
border-top: none;
height: 0;
}
.sidebar.tag-collapsed .sidebar-tree {
flex: 1 1 auto;
min-height: 0;
}
/* --- Sidebar filter --- */ /* --- Sidebar filter --- */
.sidebar-filter { .sidebar-filter {
@ -658,82 +635,77 @@ select {
color: var(--accent); color: var(--accent);
} }
.sidebar-tree { /* --- Sidebar tabs --- */
flex: 1; .sidebar-tabs {
overflow: hidden;
padding: 12px 0;
min-height: 80px;
display: flex; display: flex;
flex-direction: column; flex-shrink: 0;
border-bottom: 1px solid var(--border);
padding: 0 8px;
gap: 2px;
background: var(--bg-sidebar);
} }
.sidebar-tree::-webkit-scrollbar { .sidebar-tab {
width: 6px; flex: 1;
} display: flex;
.sidebar-tree::-webkit-scrollbar-thumb { align-items: center;
background: var(--scrollbar); justify-content: center;
border-radius: 3px; gap: 5px;
} padding: 9px 8px 8px;
border: none;
.sidebar-section-title { border-bottom: 2px solid transparent;
background: transparent;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.07em;
color: var(--text-muted);
padding: 0;
}
.sidebar-panel-toggle {
width: 100%;
border: none;
background: transparent;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 16px 10px;
cursor: pointer; cursor: pointer;
transition: color 150ms ease, background 150ms ease; transition: color 150ms ease, border-color 150ms ease;
margin-bottom: -1px;
white-space: nowrap;
} }
.sidebar-panel-toggle:hover { .sidebar-tab:hover {
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-hover);
} }
.sidebar-panel-toggle .icon, .sidebar-tab.active {
.sidebar-panel-toggle [data-lucide] { color: var(--accent);
color: inherit; border-bottom-color: var(--accent);
}
.sidebar-tab .icon {
flex-shrink: 0; flex-shrink: 0;
color: inherit;
} }
.sidebar-panel-content.collapsed { /* --- Sidebar tab panels --- */
max-height: 0 !important; .sidebar-tab-panel {
opacity: 0; display: none;
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; flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
max-height: 1000px;
scroll-behavior: smooth; scroll-behavior: smooth;
flex-direction: column;
} }
#tag-panel-content { .sidebar-tab-panel.active {
max-height: 320px; display: flex;
flex-direction: column;
}
.sidebar-tab-panel::-webkit-scrollbar {
width: 6px;
}
.sidebar-tab-panel::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border-radius: 3px;
}
#sidebar-panel-vaults .sidebar-dropdown {
padding: 10px 12px 6px;
} }
.tree-item { .tree-item {
@ -817,31 +789,32 @@ select {
.filter-result-item { .filter-result-item {
align-items: flex-start; align-items: flex-start;
gap: 8px; white-space: normal;
border-radius: 8px; overflow: visible;
border-radius: 6px;
} }
.filter-result-text { .filter-result-text {
flex: 1;
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} white-space: normal;
.filter-result-primary,
.filter-result-secondary {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
} }
.filter-result-primary { .filter-result-primary {
color: var(--text-primary); color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.filter-result-secondary { .filter-result-secondary {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.78rem; font-size: 0.75rem;
word-break: break-all;
opacity: 0.8;
} }
.sidebar-filter-empty { .sidebar-filter-empty {
@ -857,68 +830,13 @@ select {
border-radius: 4px; border-radius: 4px;
} }
/* --- Tag resize handle --- */
.tag-resize-handle {
height: 5px;
cursor: ns-resize;
background: transparent;
border-top: 1px solid var(--border);
flex-shrink: 0;
transition: background 150ms ease;
}
.tag-resize-handle:hover,
.tag-resize-handle.active {
background: var(--accent);
opacity: 0.5;
}
/* --- Tag Cloud --- */ /* --- Tag Cloud --- */
.tag-cloud-section {
padding: 12px 0 0;
height: 180px;
min-height: 60px;
max-height: 400px;
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;
}
.tag-cloud-section::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border-radius: 3px;
}
.tag-cloud-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 0;
}
.tag-cloud { .tag-cloud {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
padding: 0 16px 12px; padding: 14px 16px 16px;
} align-content: flex-start;
.tag-cloud-section:not(.collapsed-panel) #tag-panel-content {
overflow-y: auto;
flex: 1 1 auto;
} }
.tag-item { .tag-item {
@ -939,14 +857,6 @@ select {
display: none; display: none;
} }
.tag-resize-handle.hidden {
display: none;
}
.sidebar.vault-collapsed .tag-panel-toggle {
border-top: 1px solid var(--border);
}
/* --- Sidebar resize handle (horizontal) --- */ /* --- Sidebar resize handle (horizontal) --- */
.sidebar-resize-handle { .sidebar-resize-handle {
width: 5px; width: 5px;