Add desktop sidebar toggle with persistent state, search result highlighting, and improved filter icon positioning

This commit is contained in:
Bruno Charest 2026-03-22 23:37:53 -04:00
parent 8e1ae4be26
commit 6f694148db
3 changed files with 134 additions and 6 deletions

View File

@ -368,6 +368,32 @@
return res.json(); return res.json();
} }
// ---------------------------------------------------------------------------
// Sidebar toggle (desktop)
// ---------------------------------------------------------------------------
function initSidebarToggle() {
const toggleBtn = document.getElementById("sidebar-toggle-btn");
const sidebar = document.getElementById("sidebar");
const resizeHandle = document.getElementById("sidebar-resize-handle");
if (!toggleBtn || !sidebar || !resizeHandle) return;
// Restore saved state
const savedState = localStorage.getItem("obsigate-sidebar-hidden");
if (savedState === "true") {
sidebar.classList.add("hidden");
resizeHandle.classList.add("hidden");
toggleBtn.classList.add("active");
}
toggleBtn.addEventListener("click", () => {
const isHidden = sidebar.classList.toggle("hidden");
resizeHandle.classList.toggle("hidden", isHidden);
toggleBtn.classList.toggle("active", isHidden);
localStorage.setItem("obsigate-sidebar-hidden", isHidden ? "true" : "false");
});
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mobile sidebar // Mobile sidebar
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -1580,10 +1606,26 @@
const container = el("div", { class: "search-results" }); const container = el("div", { class: "search-results" });
data.results.forEach((r) => { data.results.forEach((r) => {
// Create title with highlighting
const titleDiv = el("div", { class: "search-result-title" });
if (query && query.trim()) {
highlightSearchText(titleDiv, r.title, query, searchCaseSensitive);
} else {
titleDiv.textContent = r.title;
}
// Create snippet with highlighting
const snippetDiv = el("div", { class: "search-result-snippet" });
if (query && query.trim() && r.snippet) {
highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive);
} else {
snippetDiv.textContent = r.snippet || "";
}
const item = el("div", { class: "search-result-item" }, [ const item = el("div", { class: "search-result-item" }, [
el("div", { class: "search-result-title" }, [document.createTextNode(r.title)]), titleDiv,
el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]),
el("div", { class: "search-result-snippet" }, [document.createTextNode(r.snippet || "")]), snippetDiv,
]); ]);
if (r.tags && r.tags.length > 0) { if (r.tags && r.tags.length > 0) {
@ -1764,6 +1806,40 @@
} }
} }
function highlightSearchText(container, text, query, caseSensitive) {
container.textContent = "";
if (!query || !text) {
container.appendChild(document.createTextNode(text || ""));
return;
}
const source = caseSensitive ? text : text.toLowerCase();
const needle = caseSensitive ? query : query.toLowerCase();
let start = 0;
let index = source.indexOf(needle, start);
if (index === -1) {
container.appendChild(document.createTextNode(text));
return;
}
while (index !== -1) {
if (index > start) {
container.appendChild(document.createTextNode(text.slice(start, index)));
}
const mark = el("mark", { class: "search-highlight" }, [
document.createTextNode(text.slice(index, index + query.length)),
]);
container.appendChild(mark);
start = index + query.length;
index = source.indexOf(needle, start);
}
if (start < text.length) {
container.appendChild(document.createTextNode(text.slice(start)));
}
}
function showWelcome() { function showWelcome() {
const area = document.getElementById("content-area"); const area = document.getElementById("content-area");
area.innerHTML = ` area.innerHTML = `
@ -2015,6 +2091,7 @@
document.getElementById("theme-toggle").addEventListener("click", toggleTheme); document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("header-logo").addEventListener("click", goHome); document.getElementById("header-logo").addEventListener("click", goHome);
initSearch(); initSearch();
initSidebarToggle();
initMobile(); initMobile();
initVaultContext(); initVaultContext();
initSidebarTabs(); initSidebarTabs();

View File

@ -68,6 +68,9 @@
<button class="hamburger-btn" id="hamburger-btn" title="Menu"> <button class="hamburger-btn" id="hamburger-btn" title="Menu">
<i data-lucide="menu" style="width:20px;height:20px"></i> <i data-lucide="menu" style="width:20px;height:20px"></i>
</button> </button>
<button class="sidebar-toggle-btn" id="sidebar-toggle-btn" title="Afficher/Masquer la sidebar">
<i data-lucide="sidebar" style="width:20px;height:20px"></i>
</button>
<div class="header-logo" id="header-logo"> <div class="header-logo" id="header-logo">
<i data-lucide="book-open" style="width:20px;height:20px"></i> <i data-lucide="book-open" style="width:20px;height:20px"></i>
@ -161,8 +164,8 @@
<!-- 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>
<div class="sidebar-filter-input-wrapper"> <div class="sidebar-filter-input-wrapper">
<i data-lucide="filter" class="sidebar-filter-icon" style="width:14px;height:14px"></i>
<input type="text" id="sidebar-filter-input" placeholder="Filtrer fichiers..." 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">

View File

@ -137,6 +137,25 @@ a:hover {
color: var(--accent); color: var(--accent);
} }
.sidebar-toggle-btn {
display: flex;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: color 200ms ease;
}
.sidebar-toggle-btn:hover {
color: var(--accent);
}
.sidebar-toggle-btn.active {
color: var(--accent);
}
.header-logo { .header-logo {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-weight: 700; font-weight: 700;
@ -544,10 +563,17 @@ select {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
transition: background 200ms ease; transition: background 200ms ease, transform 300ms ease;
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar.hidden {
transform: translateX(-100%);
width: 0;
min-width: 0;
border-right: none;
}
/* --- Sidebar filter --- */ /* --- Sidebar filter --- */
.sidebar-filter { .sidebar-filter {
@ -557,7 +583,6 @@ select {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px;
} }
.sidebar-filter-input-wrapper { .sidebar-filter-input-wrapper {
@ -863,7 +888,7 @@ select {
cursor: ew-resize; cursor: ew-resize;
background: transparent; background: transparent;
flex-shrink: 0; flex-shrink: 0;
transition: background 150ms ease; transition: background 150ms ease, opacity 300ms ease;
z-index: 10; z-index: 10;
} }
.sidebar-resize-handle:hover, .sidebar-resize-handle:hover,
@ -871,6 +896,11 @@ select {
background: var(--accent); background: var(--accent);
opacity: 0.5; opacity: 0.5;
} }
.sidebar-resize-handle.hidden {
width: 0;
opacity: 0;
pointer-events: none;
}
/* --- Content Area --- */ /* --- Content Area --- */
.content-area { .content-area {
@ -1238,6 +1268,20 @@ select {
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.5; line-height: 1.5;
} }
.search-result-snippet .search-highlight {
background: color-mix(in srgb, var(--accent) 25%, transparent);
color: var(--accent);
font-weight: 600;
padding: 1px 3px;
border-radius: 3px;
}
.search-result-title .search-highlight {
background: color-mix(in srgb, var(--accent) 25%, transparent);
color: var(--accent);
font-weight: 700;
padding: 1px 3px;
border-radius: 3px;
}
.search-result-tags { .search-result-tags {
margin-top: 6px; margin-top: 6px;
display: flex; display: flex;
@ -1759,6 +1803,10 @@ body.resizing-v {
display: flex; display: flex;
} }
.sidebar-toggle-btn {
display: none;
}
.header { .header {
gap: 6px; gap: 6px;
padding: 8px 10px; padding: 8px 10px;