diff --git a/frontend/app.js b/frontend/app.js index a240851..67c852f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -24,7 +24,7 @@ let _iconDebounceTimer = null; let activeSidebarTab = "vaults"; let filterDebounce = null; - + // Vault settings cache for hideHiddenFiles let vaultSettings = {}; @@ -111,29 +111,39 @@ try { const raw = localStorage.getItem(SEARCH_HISTORY_KEY); return raw ? JSON.parse(raw) : []; - } catch { return []; } + } catch { + return []; + } }, _save(entries) { - try { localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); } catch {} + try { + localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); + } catch {} + }, + getAll() { + return this._load(); }, - getAll() { return this._load(); }, add(query) { if (!query || !query.trim()) return; const q = query.trim(); - let entries = this._load().filter(e => e !== q); + let entries = this._load().filter((e) => e !== q); entries.unshift(q); if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES); this._save(entries); }, remove(query) { - const entries = this._load().filter(e => e !== query); + const entries = this._load().filter((e) => e !== query); this._save(entries); }, - clear() { this._save([]); }, + clear() { + this._save([]); + }, filter(prefix) { if (!prefix) return this.getAll().slice(0, 8); const lp = prefix.toLowerCase(); - return this._load().filter(e => e.toLowerCase().includes(lp)).slice(0, 8); + return this._load() + .filter((e) => e.toLowerCase().includes(lp)) + .slice(0, 8); }, }; @@ -176,8 +186,11 @@ if (raw[i] !== '"') { let j = i; while (j < n && raw[j] !== " ") { - if (raw[j] === '"') { j++; while (j < n && raw[j] !== '"') j++; if (j < n) j++; } - else j++; + if (raw[j] === '"') { + j++; + while (j < n && raw[j] !== '"') j++; + if (j < n) j++; + } else j++; } tokens.push(raw.slice(i, j).replace(/"/g, "")); i = j; @@ -243,8 +256,7 @@ // Close dropdown on outside click document.addEventListener("click", (e) => { - if (this._dropdown && !this._dropdown.contains(e.target) && - e.target.id !== "search-input") { + if (this._dropdown && !this._dropdown.contains(e.target) && e.target.id !== "search-input") { this.hide(); } }); @@ -267,7 +279,10 @@ /** Populate and show the dropdown with history, title suggestions, and tag suggestions */ async populate(inputValue, cursorPos) { // Cancel previous suggestion request - if (suggestAbortController) { suggestAbortController.abort(); suggestAbortController = null; } + if (suggestAbortController) { + suggestAbortController.abort(); + suggestAbortController = null; + } const ctx = QueryParser.getContext(inputValue, cursorPos); const vault = document.getElementById("vault-filter").value; @@ -304,7 +319,7 @@ try { const [titlesRes, tagsRes] = await Promise.all([ ctx.type !== "tag" ? api(`/api/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=8`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), - (ctx.type === "tag" || ctx.type === "text") ? api(`/api/tags/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=6`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), + ctx.type === "tag" || ctx.type === "text" ? api(`/api/tags/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=6`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), ]); this._renderTitles(titlesRes.suggestions || [], ctx.prefix); @@ -319,7 +334,8 @@ const historyVisible = !this._historySection.hidden; const hasAny = historyVisible || hasTitles || hasTags; this._emptyEl.hidden = hasAny; - if (hasAny) this.show(); else if (!historyVisible) this.hide(); + if (hasAny) this.show(); + else if (!historyVisible) this.hide(); this._collectItems(); } catch (err) { @@ -340,7 +356,7 @@ li.addEventListener("click", () => { const input = document.getElementById("search-input"); input.value = entry; - input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event("input", { bubbles: true })); this.hide(); _triggerAdvancedSearch(entry); }); @@ -402,7 +418,7 @@ } else { input.value = (current ? current + " " : "") + "tag:" + item.tag + " "; } - input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event("input", { bubbles: true })); this.hide(); input.focus(); _triggerAdvancedSearch(input.value); @@ -415,7 +431,10 @@ const lower = text.toLowerCase(); const needle = query.toLowerCase(); const pos = lower.indexOf(needle); - if (pos === -1) { container.textContent = text; return; } + if (pos === -1) { + container.textContent = text; + return; + } container.appendChild(document.createTextNode(text.slice(0, pos))); const markEl = el("mark", {}, [document.createTextNode(text.slice(pos, pos + query.length))]); container.appendChild(markEl); @@ -425,7 +444,7 @@ _collectItems() { dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item")); dropdownActiveIndex = -1; - dropdownItems.forEach(item => item.classList.remove("active")); + dropdownItems.forEach((item) => item.classList.remove("active")); }, navigateDown() { @@ -458,15 +477,29 @@ // --------------------------------------------------------------------------- const SearchChips = { _container: null, - init() { this._container = document.getElementById("search-chips"); }, + init() { + this._container = document.getElementById("search-chips"); + }, update(parsed) { if (!this._container) return; this._container.innerHTML = ""; let hasChips = false; - parsed.tags.forEach(tag => { this._addChip("tag", `tag:${tag}`, tag); hasChips = true; }); - if (parsed.vault) { this._addChip("vault", `vault:${parsed.vault}`, parsed.vault); hasChips = true; } - if (parsed.title) { this._addChip("title", `title:${parsed.title}`, parsed.title); hasChips = true; } - if (parsed.path) { this._addChip("path", `path:${parsed.path}`, parsed.path); hasChips = true; } + parsed.tags.forEach((tag) => { + this._addChip("tag", `tag:${tag}`, tag); + hasChips = true; + }); + if (parsed.vault) { + this._addChip("vault", `vault:${parsed.vault}`, parsed.vault); + hasChips = true; + } + if (parsed.title) { + this._addChip("title", `title:${parsed.title}`, parsed.title); + hasChips = true; + } + if (parsed.path) { + this._addChip("path", `path:${parsed.path}`, parsed.path); + hasChips = true; + } this._container.hidden = !hasChips; }, clear() { @@ -526,7 +559,11 @@ if (_iconDebounceTimer) return; // already scheduled _iconDebounceTimer = requestAnimationFrame(() => { _iconDebounceTimer = null; - try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ } + try { + lucide.createIcons(); + } catch (e) { + /* CDN not loaded */ + } }); } @@ -537,13 +574,21 @@ _iconDebounceTimer = null; } if (typeof lucide !== "undefined" && lucide.createIcons) { - try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ } + try { + lucide.createIcons(); + } catch (e) { + /* CDN not loaded */ + } } } function safeHighlight(block) { if (typeof hljs !== "undefined" && hljs.highlightElement) { - try { hljs.highlightElement(block); } catch (e) { /* CDN not loaded */ } + try { + hljs.highlightElement(block); + } catch (e) { + /* CDN not loaded */ + } } } @@ -556,26 +601,28 @@ * Slugify text to create valid IDs */ slugify(text) { - return text - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim() || 'heading'; + return ( + text + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim() || "heading" + ); }, /** * Parse headings from markdown content */ parseHeadings() { - const contentArea = document.querySelector('.md-content'); + const contentArea = document.querySelector(".md-content"); if (!contentArea) return []; const headings = []; - const h2s = contentArea.querySelectorAll('h2'); - const h3s = contentArea.querySelectorAll('h3'); + const h2s = contentArea.querySelectorAll("h2"); + const h3s = contentArea.querySelectorAll("h3"); const allHeadings = [...h2s, ...h3s].sort((a, b) => { return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; }); @@ -609,7 +656,7 @@ id, level, text, - element: heading + element: heading, }); }); @@ -620,12 +667,12 @@ * Render outline list */ renderOutline(headings) { - const outlineList = document.getElementById('outline-list'); - const outlineEmpty = document.getElementById('outline-empty'); + const outlineList = document.getElementById("outline-list"); + const outlineEmpty = document.getElementById("outline-empty"); if (!outlineList) return; - outlineList.innerHTML = ''; + outlineList.innerHTML = ""; if (!headings || headings.length === 0) { outlineList.hidden = true; @@ -640,14 +687,18 @@ if (outlineEmpty) outlineEmpty.hidden = true; headings.forEach((heading) => { - const item = el('a', { - class: `outline-item level-${heading.level}`, - href: `#${heading.id}`, - 'data-heading-id': heading.id, - role: 'link' - }, [document.createTextNode(heading.text)]); + const item = el( + "a", + { + class: `outline-item level-${heading.level}`, + href: `#${heading.id}`, + "data-heading-id": heading.id, + role: "link", + }, + [document.createTextNode(heading.text)], + ); - item.addEventListener('click', (e) => { + item.addEventListener("click", (e) => { e.preventDefault(); this.scrollToHeading(heading.id); }); @@ -665,16 +716,16 @@ const heading = document.getElementById(headingId); if (!heading) return; - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; // Calculate offset for fixed header (if any) const headerHeight = 80; const headingTop = heading.offsetTop; - + contentArea.scrollTo({ top: headingTop - headerHeight, - behavior: 'smooth' + behavior: "smooth", }); // Update active state immediately @@ -689,16 +740,16 @@ activeHeadingId = headingId; - const items = document.querySelectorAll('.outline-item'); + const items = document.querySelectorAll(".outline-item"); items.forEach((item) => { - if (item.getAttribute('data-heading-id') === headingId) { - item.classList.add('active'); - item.setAttribute('aria-current', 'location'); + if (item.getAttribute("data-heading-id") === headingId) { + item.classList.add("active"); + item.setAttribute("aria-current", "location"); // Scroll outline item into view - item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + item.scrollIntoView({ block: "nearest", behavior: "smooth" }); } else { - item.classList.remove('active'); - item.removeAttribute('aria-current'); + item.classList.remove("active"); + item.removeAttribute("aria-current"); } }); }, @@ -721,7 +772,7 @@ ReadingProgressManager.destroy(); headingsCache = []; activeHeadingId = null; - } + }, }; // --------------------------------------------------------------------------- @@ -738,13 +789,13 @@ if (!headings || headings.length === 0) return; - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; const options = { root: contentArea, - rootMargin: '-20% 0px -70% 0px', - threshold: [0, 0.3, 0.5, 1.0] + rootMargin: "-20% 0px -70% 0px", + threshold: [0, 0.3, 0.5, 1.0], }; this.observer = new IntersectionObserver((entries) => { @@ -778,7 +829,7 @@ this.observer = null; } this.headings = []; - } + }, }; // --------------------------------------------------------------------------- @@ -791,21 +842,21 @@ init() { this.destroy(); - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; this.scrollHandler = this.throttle(() => { this.updateProgress(); }, 100); - contentArea.addEventListener('scroll', this.scrollHandler); + contentArea.addEventListener("scroll", this.scrollHandler); this.updateProgress(); }, updateProgress() { - const contentArea = document.getElementById('content-area'); - const progressFill = document.getElementById('reading-progress-fill'); - const progressText = document.getElementById('reading-progress-text'); + const contentArea = document.getElementById("content-area"); + const progressFill = document.getElementById("reading-progress-fill"); + const progressText = document.getElementById("reading-progress-text"); if (!contentArea || !progressFill || !progressText) return; @@ -822,7 +873,7 @@ throttle(func, delay) { let lastCall = 0; - return function(...args) { + return function (...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; @@ -832,18 +883,18 @@ }, destroy() { - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (contentArea && this.scrollHandler) { - contentArea.removeEventListener('scroll', this.scrollHandler); + contentArea.removeEventListener("scroll", this.scrollHandler); } this.scrollHandler = null; // Reset progress - const progressFill = document.getElementById('reading-progress-fill'); - const progressText = document.getElementById('reading-progress-text'); - if (progressFill) progressFill.style.width = '0%'; - if (progressText) progressText.textContent = '0%'; - } + const progressFill = document.getElementById("reading-progress-fill"); + const progressText = document.getElementById("reading-progress-text"); + if (progressFill) progressFill.style.width = "0%"; + if (progressText) progressText.textContent = "0%"; + }, }; // --------------------------------------------------------------------------- @@ -858,11 +909,11 @@ }, loadState() { - const savedVisible = localStorage.getItem('obsigate-right-sidebar-visible'); - const savedWidth = localStorage.getItem('obsigate-right-sidebar-width'); + const savedVisible = localStorage.getItem("obsigate-right-sidebar-visible"); + const savedWidth = localStorage.getItem("obsigate-right-sidebar-width"); if (savedVisible !== null) { - rightSidebarVisible = savedVisible === 'true'; + rightSidebarVisible = savedVisible === "true"; } if (savedWidth) { @@ -873,58 +924,58 @@ }, applyState() { - const sidebar = document.getElementById('right-sidebar'); - const handle = document.getElementById('right-sidebar-resize-handle'); - const tocBtn = document.getElementById('toc-toggle-btn'); - const headerToggleBtn = document.getElementById('right-sidebar-toggle-btn'); + const sidebar = document.getElementById("right-sidebar"); + const handle = document.getElementById("right-sidebar-resize-handle"); + const tocBtn = document.getElementById("toc-toggle-btn"); + const headerToggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (!sidebar) return; if (rightSidebarVisible) { - sidebar.classList.remove('hidden'); + sidebar.classList.remove("hidden"); sidebar.style.width = `${rightSidebarWidth}px`; - if (handle) handle.classList.remove('hidden'); + if (handle) handle.classList.remove("hidden"); if (tocBtn) { - tocBtn.classList.add('active'); - tocBtn.title = 'Masquer le sommaire'; + tocBtn.classList.add("active"); + tocBtn.title = "Masquer le sommaire"; } if (headerToggleBtn) { - headerToggleBtn.title = 'Masquer le panneau'; - headerToggleBtn.setAttribute('aria-label', 'Masquer le panneau'); + headerToggleBtn.title = "Masquer le panneau"; + headerToggleBtn.setAttribute("aria-label", "Masquer le panneau"); } } else { - sidebar.classList.add('hidden'); - if (handle) handle.classList.add('hidden'); + sidebar.classList.add("hidden"); + if (handle) handle.classList.add("hidden"); if (tocBtn) { - tocBtn.classList.remove('active'); - tocBtn.title = 'Afficher le sommaire'; + tocBtn.classList.remove("active"); + tocBtn.title = "Afficher le sommaire"; } if (headerToggleBtn) { - headerToggleBtn.title = 'Afficher le panneau'; - headerToggleBtn.setAttribute('aria-label', 'Afficher le panneau'); + headerToggleBtn.title = "Afficher le panneau"; + headerToggleBtn.setAttribute("aria-label", "Afficher le panneau"); } } - + // Update icons safeCreateIcons(); }, toggle() { rightSidebarVisible = !rightSidebarVisible; - localStorage.setItem('obsigate-right-sidebar-visible', rightSidebarVisible); + localStorage.setItem("obsigate-right-sidebar-visible", rightSidebarVisible); this.applyState(); }, initToggle() { - const toggleBtn = document.getElementById('right-sidebar-toggle-btn'); + const toggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (toggleBtn) { - toggleBtn.addEventListener('click', () => this.toggle()); + toggleBtn.addEventListener("click", () => this.toggle()); } }, initResize() { - const handle = document.getElementById('right-sidebar-resize-handle'); - const sidebar = document.getElementById('right-sidebar'); + const handle = document.getElementById("right-sidebar-resize-handle"); + const sidebar = document.getElementById("right-sidebar"); if (!handle || !sidebar) return; @@ -936,9 +987,9 @@ isResizing = true; startX = e.clientX; startWidth = sidebar.offsetWidth; - handle.classList.add('active'); - document.body.style.cursor = 'ew-resize'; - document.body.style.userSelect = 'none'; + handle.classList.add("active"); + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; }; const onMouseMove = (e) => { @@ -958,17 +1009,17 @@ if (!isResizing) return; isResizing = false; - handle.classList.remove('active'); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; + handle.classList.remove("active"); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; - localStorage.setItem('obsigate-right-sidebar-width', rightSidebarWidth); + localStorage.setItem("obsigate-right-sidebar-width", rightSidebarWidth); }; - handle.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - } + handle.addEventListener("mousedown", onMouseDown); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }, }; // --------------------------------------------------------------------------- @@ -1047,30 +1098,30 @@ // Custom Dropdowns // --------------------------------------------------------------------------- function initCustomDropdowns() { - document.querySelectorAll('.custom-dropdown').forEach(dropdown => { - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - const options = dropdown.querySelectorAll('.custom-dropdown-option'); + document.querySelectorAll(".custom-dropdown").forEach((dropdown) => { + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + const options = dropdown.querySelectorAll(".custom-dropdown-option"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); - const selectedText = dropdown.querySelector('.custom-dropdown-selected'); - const menu = dropdown.querySelector('.custom-dropdown-menu'); + const selectedText = dropdown.querySelector(".custom-dropdown-selected"); + const menu = dropdown.querySelector(".custom-dropdown-menu"); if (!trigger) return; // Toggle dropdown - trigger.addEventListener('click', (e) => { + trigger.addEventListener("click", (e) => { e.stopPropagation(); - const isOpen = dropdown.classList.contains('open'); - + const isOpen = dropdown.classList.contains("open"); + // Close all other dropdowns - document.querySelectorAll('.custom-dropdown.open').forEach(d => { - if (d !== dropdown) d.classList.remove('open'); + document.querySelectorAll(".custom-dropdown.open").forEach((d) => { + if (d !== dropdown) d.classList.remove("open"); }); - - dropdown.classList.toggle('open', !isOpen); - trigger.setAttribute('aria-expanded', !isOpen); + + dropdown.classList.toggle("open", !isOpen); + trigger.setAttribute("aria-expanded", !isOpen); // Position fixed menu for sidebar dropdowns - if (!isOpen && dropdown.classList.contains('sidebar-dropdown') && menu) { + if (!isOpen && dropdown.classList.contains("sidebar-dropdown") && menu) { const rect = trigger.getBoundingClientRect(); menu.style.top = `${rect.bottom + 4}px`; menu.style.left = `${rect.left}px`; @@ -1079,17 +1130,17 @@ }); // Handle option selection - options.forEach(option => { - option.addEventListener('click', (e) => { + options.forEach((option) => { + option.addEventListener("click", (e) => { e.stopPropagation(); - const value = option.getAttribute('data-value'); + const value = option.getAttribute("data-value"); const text = option.textContent; // Update hidden input if (hiddenInput) { hiddenInput.value = value; // Trigger change event - hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } // Update selected text @@ -1098,22 +1149,22 @@ } // Update visual selection - options.forEach(opt => opt.classList.remove('selected')); - option.classList.add('selected'); + options.forEach((opt) => opt.classList.remove("selected")); + option.classList.add("selected"); // Close dropdown - dropdown.classList.remove('open'); - trigger.setAttribute('aria-expanded', 'false'); + dropdown.classList.remove("open"); + trigger.setAttribute("aria-expanded", "false"); }); }); }); // Close dropdowns when clicking outside - document.addEventListener('click', () => { - document.querySelectorAll('.custom-dropdown.open').forEach(dropdown => { - dropdown.classList.remove('open'); - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - if (trigger) trigger.setAttribute('aria-expanded', 'false'); + document.addEventListener("click", () => { + document.querySelectorAll(".custom-dropdown.open").forEach((dropdown) => { + dropdown.classList.remove("open"); + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } @@ -1123,24 +1174,24 @@ const dropdown = document.getElementById(dropdownId); if (!dropdown) return; - const optionsContainer = dropdown.querySelector('.custom-dropdown-menu'); + const optionsContainer = dropdown.querySelector(".custom-dropdown-menu"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); - const selectedText = dropdown.querySelector('.custom-dropdown-selected'); + const selectedText = dropdown.querySelector(".custom-dropdown-selected"); if (!optionsContainer) return; // Clear existing options (keep the first one if it's the default) - optionsContainer.innerHTML = ''; + optionsContainer.innerHTML = ""; // Add new options - optionsList.forEach(opt => { - const li = document.createElement('li'); - li.className = 'custom-dropdown-option'; - li.setAttribute('role', 'option'); - li.setAttribute('data-value', opt.value); + optionsList.forEach((opt) => { + const li = document.createElement("li"); + li.className = "custom-dropdown-option"; + li.setAttribute("role", "option"); + li.setAttribute("data-value", opt.value); li.textContent = opt.text; if (opt.value === defaultValue) { - li.classList.add('selected'); + li.classList.add("selected"); if (selectedText) selectedText.textContent = opt.text; if (hiddenInput) hiddenInput.value = opt.value; } @@ -1148,27 +1199,27 @@ }); // Re-initialize click handlers - optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(option => { - option.addEventListener('click', (e) => { + optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((option) => { + option.addEventListener("click", (e) => { e.stopPropagation(); - const value = option.getAttribute('data-value'); + const value = option.getAttribute("data-value"); const text = option.textContent; if (hiddenInput) { hiddenInput.value = value; - hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } if (selectedText) { selectedText.textContent = text; } - optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(opt => opt.classList.remove('selected')); - option.classList.add('selected'); + optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((opt) => opt.classList.remove("selected")); + option.classList.add("selected"); - dropdown.classList.remove('open'); - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - if (trigger) trigger.setAttribute('aria-expanded', 'false'); + dropdown.classList.remove("open"); + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } @@ -1194,10 +1245,14 @@ toast.textContent = message; container.appendChild(toast); // Trigger entrance animation - requestAnimationFrame(function () { toast.classList.add("show"); }); + requestAnimationFrame(function () { + toast.classList.add("show"); + }); setTimeout(function () { toast.classList.remove("show"); - toast.addEventListener("transitionend", function () { toast.remove(); }); + toast.addEventListener("transitionend", function () { + toast.remove(); + }); }, 3500); } @@ -1222,7 +1277,7 @@ if (authHeaders) { mergedOpts.headers = { ...mergedOpts.headers, ...authHeaders }; } - mergedOpts.credentials = 'include'; + mergedOpts.credentials = "include"; res = await fetch(path, mergedOpts); } catch (err) { if (err.name === "AbortError") throw err; // let callers handle abort @@ -1237,17 +1292,22 @@ const retryHeaders = AuthManager.getAuthHeaders(); const retryOpts = opts || {}; retryOpts.headers = { ...retryOpts.headers, ...retryHeaders }; - retryOpts.credentials = 'include'; + retryOpts.credentials = "include"; res = await fetch(path, retryOpts); } catch (refreshErr) { AuthManager.clearSession(); AuthManager.showLoginScreen(); - throw new Error('Session expirée'); + throw new Error("Session expirée"); } } if (!res.ok) { var detail = ""; - try { var body = await res.json(); detail = body.detail || ""; } catch (_) { /* no json body */ } + try { + var body = await res.json(); + detail = body.detail || ""; + } catch (_) { + /* no json body */ + } showToast(detail || "Erreur API : " + res.status, "error"); throw new Error(detail || "API error: " + res.status); } @@ -1259,15 +1319,15 @@ // --------------------------------------------------------------------------- const AuthManager = { - ACCESS_TOKEN_KEY: 'obsigate_access_token', - TOKEN_EXPIRY_KEY: 'obsigate_token_expiry', - USER_KEY: 'obsigate_user', + ACCESS_TOKEN_KEY: "obsigate_access_token", + TOKEN_EXPIRY_KEY: "obsigate_token_expiry", + USER_KEY: "obsigate_user", _authEnabled: false, // ── Token storage (sessionStorage) ───────────────────────────── saveToken(tokenData) { - const expiresAt = Date.now() + (tokenData.expires_in * 1000); + const expiresAt = Date.now() + tokenData.expires_in * 1000; sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token); sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString()); if (tokenData.user) { @@ -1288,7 +1348,7 @@ const expiry = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY); if (!expiry) return true; // Renew 60s before expiration - return Date.now() > (parseInt(expiry) - 60000); + return Date.now() > parseInt(expiry) - 60000; }, clearSession() { @@ -1300,21 +1360,21 @@ getAuthHeaders() { const token = this.getToken(); if (!token || !this._authEnabled) return null; - return { 'Authorization': 'Bearer ' + token }; + return { Authorization: "Bearer " + token }; }, // ── API calls ────────────────────────────────────────────────── async login(username, password, rememberMe) { - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ username, password, remember_me: rememberMe || false }), }); if (!response.ok) { const err = await response.json(); - throw new Error(err.detail || 'Erreur de connexion'); + throw new Error(err.detail || "Erreur de connexion"); } const data = await response.json(); this.saveToken(data); @@ -1324,27 +1384,29 @@ async logout() { try { const token = this.getToken(); - await fetch('/api/auth/logout', { - method: 'POST', - headers: token ? { 'Authorization': 'Bearer ' + token } : {}, - credentials: 'include', + await fetch("/api/auth/logout", { + method: "POST", + headers: token ? { Authorization: "Bearer " + token } : {}, + credentials: "include", }); - } catch (e) { /* continue even if API fails */ } + } catch (e) { + /* continue even if API fails */ + } this.clearSession(); this.showLoginScreen(); }, async refreshAccessToken() { - const response = await fetch('/api/auth/refresh', { - method: 'POST', - credentials: 'include', + const response = await fetch("/api/auth/refresh", { + method: "POST", + credentials: "include", }); if (!response.ok) { this.clearSession(); - throw new Error('Session expirée'); + throw new Error("Session expirée"); } const data = await response.json(); - const expiry = Date.now() + (data.expires_in * 1000); + const expiry = Date.now() + data.expires_in * 1000; sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token); sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString()); return data.access_token; @@ -1353,48 +1415,49 @@ // ── UI controls ──────────────────────────────────────────────── showLoginScreen() { - const app = document.getElementById('app'); - const login = document.getElementById('login-screen'); - if (app) app.classList.add('hidden'); + const app = document.getElementById("app"); + const login = document.getElementById("login-screen"); + if (app) app.classList.add("hidden"); if (login) { - login.classList.remove('hidden'); - const usernameInput = document.getElementById('login-username'); + login.classList.remove("hidden"); + const usernameInput = document.getElementById("login-username"); if (usernameInput) usernameInput.focus(); } }, showApp() { - const login = document.getElementById('login-screen'); - const app = document.getElementById('app'); - if (login) login.classList.add('hidden'); - if (app) app.classList.remove('hidden'); + const login = document.getElementById("login-screen"); + const app = document.getElementById("app"); + if (login) login.classList.add("hidden"); + if (app) app.classList.remove("hidden"); this.renderUserMenu(); }, renderUserMenu() { const user = this.getUser(); - const userMenu = document.getElementById('user-menu'); + const userMenu = document.getElementById("user-menu"); if (!userMenu) return; if (!user || !this._authEnabled) { - userMenu.innerHTML = ''; + userMenu.innerHTML = ""; return; } - userMenu.innerHTML = - '' + (user.display_name || user.username) + '' + - ''; + userMenu.innerHTML = '' + (user.display_name || user.username) + "" + ''; safeCreateIcons(); - const logoutBtn = document.getElementById('logout-btn'); - if (logoutBtn) logoutBtn.addEventListener('click', () => AuthManager.logout()); + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout()); - const adminRow = document.getElementById('admin-menu-row'); + const adminRow = document.getElementById("admin-menu-row"); if (adminRow) { - if (user.role === 'admin') { - adminRow.classList.remove('hidden'); + if (user.role === "admin") { + adminRow.classList.remove("hidden"); // Important: use an inline function to ensure we don't bind multiple identical listeners on rerenders, or clean up before - adminRow.onclick = () => { closeHeaderMenu(); AdminPanel.show(); }; + adminRow.onclick = () => { + closeHeaderMenu(); + AdminPanel.show(); + }; } else { - adminRow.classList.add('hidden'); + adminRow.classList.add("hidden"); } } }, @@ -1403,7 +1466,7 @@ async checkAuthStatus() { try { - const res = await fetch('/api/auth/status'); + const res = await fetch("/api/auth/status"); const data = await res.json(); this._authEnabled = data.auth_enabled; return data; @@ -1432,9 +1495,9 @@ await this.refreshAccessToken(); // Fetch user info const token = this.getToken(); - const res = await fetch('/api/auth/me', { - headers: { 'Authorization': 'Bearer ' + token }, - credentials: 'include', + const res = await fetch("/api/auth/me", { + headers: { Authorization: "Bearer " + token }, + credentials: "include", }); if (res.ok) { const user = await res.json(); @@ -1442,7 +1505,9 @@ this.showApp(); return true; } - } catch (e) { /* silent refresh failed */ } + } catch (e) { + /* silent refresh failed */ + } // No valid session — show login this.showLoginScreen(); @@ -1455,21 +1520,21 @@ // --------------------------------------------------------------------------- function initLoginForm() { - const form = document.getElementById('login-form'); + const form = document.getElementById("login-form"); if (!form) return; - form.addEventListener('submit', async (e) => { + form.addEventListener("submit", async (e) => { e.preventDefault(); - const username = document.getElementById('login-username').value; - const password = document.getElementById('login-password').value; - const rememberMe = document.getElementById('remember-me').checked; - const errorEl = document.getElementById('login-error'); - const btn = document.getElementById('login-btn'); + const username = document.getElementById("login-username").value; + const password = document.getElementById("login-password").value; + const rememberMe = document.getElementById("remember-me").checked; + const errorEl = document.getElementById("login-error"); + const btn = document.getElementById("login-btn"); btn.disabled = true; - btn.querySelector('.btn-spinner').classList.remove('hidden'); - btn.querySelector('.btn-text').textContent = 'Connexion...'; - errorEl.classList.add('hidden'); + btn.querySelector(".btn-spinner").classList.remove("hidden"); + btn.querySelector(".btn-text").textContent = "Connexion..."; + errorEl.classList.add("hidden"); try { await AuthManager.login(username, password, rememberMe); @@ -1478,27 +1543,27 @@ try { await Promise.all([loadVaults(), loadTags()]); } catch (err) { - console.error('Failed to load data after login:', err); + console.error("Failed to load data after login:", err); } safeCreateIcons(); } catch (err) { errorEl.textContent = err.message; - errorEl.classList.remove('hidden'); - document.getElementById('login-password').value = ''; - document.getElementById('login-password').focus(); + errorEl.classList.remove("hidden"); + document.getElementById("login-password").value = ""; + document.getElementById("login-password").focus(); } finally { btn.disabled = false; - btn.querySelector('.btn-spinner').classList.add('hidden'); - btn.querySelector('.btn-text').textContent = 'Se connecter'; + btn.querySelector(".btn-spinner").classList.add("hidden"); + btn.querySelector(".btn-text").textContent = "Se connecter"; } }); // Toggle password visibility - const toggleBtn = document.getElementById('toggle-password'); + const toggleBtn = document.getElementById("toggle-password"); if (toggleBtn) { - toggleBtn.addEventListener('click', () => { - const input = document.getElementById('login-password'); - input.type = input.type === 'password' ? 'text' : 'password'; + toggleBtn.addEventListener("click", () => { + const input = document.getElementById("login-password"); + input.type = input.type === "password" ? "text" : "password"; }); } } @@ -1513,19 +1578,19 @@ show() { this._createModal(); - this._modal.classList.add('active'); + this._modal.classList.add("active"); this._loadUsers(); }, hide() { - if (this._modal) this._modal.classList.remove('active'); + if (this._modal) this._modal.classList.remove("active"); }, _createModal() { if (this._modal) return; - this._modal = document.createElement('div'); - this._modal.className = 'editor-modal'; - this._modal.id = 'admin-modal'; + this._modal = document.createElement("div"); + this._modal.className = "editor-modal"; + this._modal.id = "admin-modal"; this._modal.innerHTML = `
@@ -1547,90 +1612,110 @@ document.body.appendChild(this._modal); safeCreateIcons(); - document.getElementById('admin-close').addEventListener('click', () => this.hide()); - document.getElementById('admin-add-user').addEventListener('click', () => this._showUserForm(null)); + document.getElementById("admin-close").addEventListener("click", () => this.hide()); + document.getElementById("admin-add-user").addEventListener("click", () => this._showUserForm(null)); }, async _loadUsers() { try { - const users = await api('/api/auth/admin/users'); + const users = await api("/api/auth/admin/users"); // Also load available vaults try { - const vaultsData = await api('/api/vaults'); - this._allVaults = vaultsData.map(v => v.name); - } catch (e) { this._allVaults = []; } + const vaultsData = await api("/api/vaults"); + this._allVaults = vaultsData.map((v) => v.name); + } catch (e) { + this._allVaults = []; + } this._renderUsers(users); } catch (err) { - document.getElementById('admin-users-list').innerHTML = - '

Erreur : ' + err.message + '

'; + document.getElementById("admin-users-list").innerHTML = '

Erreur : ' + err.message + "

"; } }, _renderUsers(users) { - const container = document.getElementById('admin-users-list'); + const container = document.getElementById("admin-users-list"); if (!users.length) { container.innerHTML = '

Aucun utilisateur.

'; return; } - let html = '' + - '' + - ''; - users.forEach(u => { - const vaults = u.vaults.includes('*') ? 'Toutes' : (u.vaults.join(', ') || 'Aucune'); - const status = u.active ? '✅' : '🔴'; - const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString('fr-FR', { day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit' }) : 'Jamais'; - html += '' + - '' + - '' + - '' + - '' + - '' + + let html = '
UtilisateurRôleVaultsStatutDernière connexionActions
' + u.username + '' + (u.display_name && u.display_name !== u.username ? '
' + u.display_name + '' : '') + '
' + u.role + '' + vaults + '' + status + '' + lastLogin + '
' + "" + ""; + users.forEach((u) => { + const vaults = u.vaults.includes("*") ? "Toutes" : u.vaults.join(", ") || "Aucune"; + const status = u.active ? "✅" : "🔴"; + const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "Jamais"; + html += + "" + + "" + + '" + + '" + + "" + + "" + ''; + '' + + '' + + ""; }); - html += '
UtilisateurRôleVaultsStatutDernière connexionActions
" + + u.username + + "" + + (u.display_name && u.display_name !== u.username ? "
" + u.display_name + "" : "") + + "
' + + u.role + + "' + + vaults + + "" + + status + + "" + + lastLogin + + "' + - '' + - '' + - '
'; + html += ""; container.innerHTML = html; // Bind action buttons - container.querySelectorAll('[data-action="edit"]').forEach(btn => { - btn.addEventListener('click', () => { - const user = users.find(u => u.username === btn.dataset.username); + container.querySelectorAll('[data-action="edit"]').forEach((btn) => { + btn.addEventListener("click", () => { + const user = users.find((u) => u.username === btn.dataset.username); if (user) this._showUserForm(user); }); }); - container.querySelectorAll('[data-action="delete"]').forEach(btn => { - btn.addEventListener('click', () => this._deleteUser(btn.dataset.username)); + container.querySelectorAll('[data-action="delete"]').forEach((btn) => { + btn.addEventListener("click", () => this._deleteUser(btn.dataset.username)); }); }, _showUserForm(user) { const isEdit = !!user; - const title = isEdit ? 'Modifier : ' + user.username : 'Nouvel utilisateur'; - const vaultCheckboxes = this._allVaults.map(v => { - const checked = user && (user.vaults.includes(v) || user.vaults.includes('*')) ? 'checked' : ''; - return ''; - }).join(''); - const allVaultsChecked = user && user.vaults.includes('*') ? 'checked' : ''; + const title = isEdit ? "Modifier : " + user.username : "Nouvel utilisateur"; + const vaultCheckboxes = this._allVaults + .map((v) => { + const checked = user && (user.vaults.includes(v) || user.vaults.includes("*")) ? "checked" : ""; + return '"; + }) + .join(""); + const allVaultsChecked = user && user.vaults.includes("*") ? "checked" : ""; // Create form modal overlay - const overlay = document.createElement('div'); - overlay.className = 'admin-form-overlay'; + const overlay = document.createElement("div"); + overlay.className = "admin-form-overlay"; overlay.innerHTML = `

${title}

- ${!isEdit ? '
' : ''} -
-
-
+ ${!isEdit ? '
' : ""} +
+
+
${vaultCheckboxes}
- ${isEdit ? '
' : ''} + ${isEdit ? '
" : ""}
@@ -1640,15 +1725,13 @@ `; this._modal.appendChild(overlay); - document.getElementById('admin-form-cancel').addEventListener('click', () => overlay.remove()); + document.getElementById("admin-form-cancel").addEventListener("click", () => overlay.remove()); - document.getElementById('admin-user-form').addEventListener('submit', async (e) => { + document.getElementById("admin-user-form").addEventListener("submit", async (e) => { e.preventDefault(); const form = e.target; - const allVaults = document.getElementById('admin-all-vaults').checked; - const selectedVaults = allVaults - ? ['*'] - : Array.from(form.querySelectorAll('input[name="vault"]:checked')).map(cb => cb.value); + const allVaults = document.getElementById("admin-all-vaults").checked; + const selectedVaults = allVaults ? ["*"] : Array.from(form.querySelectorAll('input[name="vault"]:checked')).map((cb) => cb.value); try { if (isEdit) { @@ -1660,15 +1743,15 @@ if (form.password.value) updates.password = form.password.value; const activeCheckbox = form.querySelector('input[name="active"]'); if (activeCheckbox) updates.active = activeCheckbox.checked; - await api('/api/auth/admin/users/' + user.username, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + await api("/api/auth/admin/users/" + user.username, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); } else { - await api('/api/auth/admin/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + await api("/api/auth/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: form.username.value, password: form.password.value, @@ -1680,9 +1763,9 @@ } overlay.remove(); this._loadUsers(); - showToast(isEdit ? 'Utilisateur modifié' : 'Utilisateur créé', 'success'); + showToast(isEdit ? "Utilisateur modifié" : "Utilisateur créé", "success"); } catch (err) { - showToast(err.message, 'error'); + showToast(err.message, "error"); } }); }, @@ -1690,16 +1773,16 @@ async _deleteUser(username) { const currentUser = AuthManager.getUser(); if (currentUser && currentUser.username === username) { - showToast('Impossible de supprimer son propre compte', 'error'); + showToast("Impossible de supprimer son propre compte", "error"); return; } - if (!confirm('Supprimer l\'utilisateur "' + username + '" ?')) return; + if (!confirm("Supprimer l'utilisateur \"" + username + '" ?')) return; try { - await api('/api/auth/admin/users/' + username, { method: 'DELETE' }); + await api("/api/auth/admin/users/" + username, { method: "DELETE" }); this._loadUsers(); - showToast('Utilisateur supprimé', 'success'); + showToast("Utilisateur supprimé", "success"); } catch (err) { - showToast(err.message, 'error'); + showToast(err.message, "error"); } }, }; @@ -1711,9 +1794,9 @@ 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") { @@ -1721,7 +1804,7 @@ resizeHandle.classList.add("hidden"); toggleBtn.classList.add("active"); } - + toggleBtn.addEventListener("click", () => { const isHidden = sidebar.classList.toggle("hidden"); resizeHandle.classList.toggle("hidden", isHidden); @@ -1790,10 +1873,10 @@ const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); const contextText = document.getElementById("vault-context-text"); - + if (filter) filter.value = selectedContextVault; if (quickSelect) quickSelect.value = selectedContextVault; - + // Update vault context indicator if (contextText) { contextText.textContent = selectedContextVault === "all" ? "All" : selectedContextVault; @@ -1814,9 +1897,7 @@ const currentTop = scrollContainer.scrollTop; const offsetTop = element.offsetTop; - const targetTop = alignToTop - ? Math.max(0, offsetTop - 60) - : Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height * 0.35)); + const targetTop = alignToTop ? Math.max(0, offsetTop - 60) : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35); scrollContainer.scrollTo({ top: targetTop, @@ -1828,17 +1909,10 @@ const container = document.getElementById("vault-tree"); container.innerHTML = ""; - const vaultsToShow = selectedContextVault === "all" - ? allVaults - : allVaults.filter((v) => v.name === selectedContextVault); + const vaultsToShow = selectedContextVault === "all" ? allVaults : allVaults.filter((v) => v.name === selectedContextVault); vaultsToShow.forEach((v) => { - const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [ - icon("chevron-right", 14), - getVaultIcon(v.name, 16), - document.createTextNode(` ${v.name} `), - smallBadge(v.file_count), - ]); + const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), document.createTextNode(` ${v.name} `), smallBadge(v.file_count)]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); container.appendChild(vaultItem); @@ -1875,23 +1949,23 @@ function shouldDisplayPath(path, vaultName) { // Get hideHiddenFiles setting for this vault (default: false = show all) const settings = vaultSettings[vaultName] || { hideHiddenFiles: false }; - + if (!settings.hideHiddenFiles) { // Show all files return true; } - + // Check if any segment of the path starts with a dot (hidden) - const segments = path.split('/').filter(Boolean); + const segments = path.split("/").filter(Boolean); for (const segment of segments) { - if (segment.startsWith('.')) { + if (segment.startsWith(".")) { return false; // Hide this path } } - + return true; // Show this path } - + async function loadVaultSettings() { try { const settings = await api("/api/vaults/settings/all"); @@ -1912,10 +1986,7 @@ container.innerHTML = ""; // Prepare dropdown options - const dropdownOptions = [ - { value: "all", text: "Tous les vaults" }, - ...vaults.map(v => ({ value: v.name, text: v.name })) - ]; + const dropdownOptions = [{ value: "all", text: "Tous les vaults" }, ...vaults.map((v) => ({ value: v.name, text: v.name }))]; // Populate custom dropdowns populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all"); @@ -1923,12 +1994,7 @@ vaults.forEach((v) => { // Sidebar tree entry - const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [ - icon("chevron-right", 14), - getVaultIcon(v.name, 16), - document.createTextNode(` ${v.name} `), - smallBadge(v.file_count), - ]); + const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), document.createTextNode(` ${v.name} `), smallBadge(v.file_count)]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); container.appendChild(vaultItem); @@ -1946,17 +2012,21 @@ */ async function refreshSidebarTreePreservingState() { // 1. Capture expanded states - const expandedVaults = Array.from(document.querySelectorAll(".vault-item")).filter(v => { - const children = document.getElementById(`vault-children-${v.dataset.vault}`); - return children && !children.classList.contains("collapsed"); - }).map(v => v.dataset.vault); + const expandedVaults = Array.from(document.querySelectorAll(".vault-item")) + .filter((v) => { + const children = document.getElementById(`vault-children-${v.dataset.vault}`); + return children && !children.classList.contains("collapsed"); + }) + .map((v) => v.dataset.vault); - const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]")).filter(item => { - const vault = item.dataset.vault; - const path = item.dataset.path; - const children = document.getElementById(`dir-${vault}-${path}`); - return children && !children.classList.contains("collapsed"); - }).map(item => ({ vault: item.dataset.vault, path: item.dataset.path })); + const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]")) + .filter((item) => { + const vault = item.dataset.vault; + const path = item.dataset.path; + const children = document.getElementById(`dir-${vault}-${path}`); + return children && !children.classList.contains("collapsed"); + }) + .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path })); const selectedItem = document.querySelector(".tree-item.path-selected"); const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null; @@ -1965,7 +2035,7 @@ try { const vaults = await api("/api/vaults"); allVaults = vaults; - vaults.forEach(v => { + vaults.forEach((v) => { const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`); if (vItem) { const badge = vItem.querySelector(".badge-small"); @@ -2008,7 +2078,7 @@ if (selectedState) { await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false }); } - + safeCreateIcons(); } @@ -2128,14 +2198,9 @@ if (!shouldDisplayPath(item.path, vaultName)) { return; // Skip this item } - + if (item.type === "directory") { - const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [ - icon("chevron-right", 14), - icon("folder", 16), - document.createTextNode(` ${item.name} `), - smallBadge(item.children_count), - ]); + const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), document.createTextNode(` ${item.name} `), smallBadge(item.children_count)]); fragment.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); @@ -2161,10 +2226,7 @@ } else { const fileIconName = getFileIcon(item.name); const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name; - const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [ - icon(fileIconName, 16), - document.createTextNode(` ${displayName}`), - ]); + const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), document.createTextNode(` ${displayName}`)]); fileItem.addEventListener("click", () => { scrollTreeItemIntoView(fileItem, false); openFile(vaultName, item.path); @@ -2242,7 +2304,7 @@ await restoreSidebarTree(); return; } - + try { const vaultParam = selectedContextVault === "all" ? "all" : selectedContextVault; const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`; @@ -2274,32 +2336,28 @@ }); if (grouped.size === 0) { - container.appendChild(el("div", { class: "sidebar-filter-empty" }, [ - document.createTextNode("Aucun répertoire ou fichier correspondant."), - ])); + container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")])); return; } grouped.forEach((entries, vaultName) => { entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" })); - const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [ - getVaultIcon(vaultName, 16), - document.createTextNode(` ${vaultName} `), - smallBadge(entries.length), - ]); + const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]); container.appendChild(vaultHeader); const resultsWrapper = el("div", { class: "filter-results-group" }); entries.forEach((entry) => { - const resultItem = el("div", { - class: `tree-item filter-result-item filter-result-${entry.type}`, - "data-vault": entry.vault, - "data-path": entry.path, - "data-type": entry.type, - }, [ - icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16), - ]); + const resultItem = el( + "div", + { + class: `tree-item filter-result-item filter-result-${entry.type}`, + "data-vault": entry.vault, + "data-path": entry.path, + "data-type": entry.type, + }, + [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)], + ); const textWrap = el("div", { class: "filter-result-text" }); const primary = el("div", { class: "filter-result-primary" }); @@ -2354,14 +2412,14 @@ // Second pass: find matching items and mark them + ancestors + descendants const matchingItems = new Set(); - + items.forEach((item) => { const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase(); const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase(); if (text.includes(searchQuery)) { matchingItems.add(item); item.classList.remove("filtered-out"); - + // Show all ancestor containers let parent = item.parentElement; while (parent && parent !== tree) { @@ -2371,7 +2429,7 @@ } parent = parent.parentElement; } - + // If this is a directory (has a children container after it), show all descendants const nextEl = item.nextElementSibling; if (nextEl && nextEl.classList.contains("tree-children")) { @@ -2382,7 +2440,7 @@ } } }); - + // Third pass: show items that are descendants of matching directories // and ensure their containers are visible matchingItems.forEach((item) => { @@ -2393,7 +2451,7 @@ } }); } - + function showAllDescendants(container) { const items = container.querySelectorAll(".tree-item"); items.forEach((item) => { @@ -2433,7 +2491,7 @@ defaultFilters: [ { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true }, { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true }, - { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true } + { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true }, ], getConfig() { @@ -2455,15 +2513,15 @@ patternToRegex(pattern) { // 1. Escape ALL special regex characters // We use a broader set including * and . - let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - + let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // 2. Convert escaped '*' to '.*' (wildcard) - regex = regex.replace(/\\\*/g, '.*'); - + regex = regex.replace(/\\\*/g, ".*"); + // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*' // We also handle optional whitespace around it to make it more user-friendly - regex = regex.replace(/\s*\\\.{2,}\s*/g, '.*'); - + regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*"); + return regex; }, @@ -2471,15 +2529,15 @@ const config = this.getConfig(); const filters = config.tagFilters || this.defaultFilters; const tagWithHash = `#${tag}`; - + for (const filter of filters) { if (!filter.enabled) continue; try { // Robustly handle regex with or without ^/$ let patternStr = filter.regex; - if (!patternStr.startsWith('^')) patternStr = '^' + patternStr; - if (!patternStr.endsWith('$')) patternStr = patternStr + '$'; - + if (!patternStr.startsWith("^")) patternStr = "^" + patternStr; + if (!patternStr.endsWith("$")) patternStr = patternStr + "$"; + const regex = new RegExp(patternStr); if (regex.test(tagWithHash)) { return true; @@ -2499,7 +2557,7 @@ } }); return filtered; - } + }, }; // --------------------------------------------------------------------------- @@ -2525,9 +2583,7 @@ Object.entries(tags).forEach(([tag, count]) => { const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0; const size = minSize + ratio * (maxSize - minSize); - const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [ - document.createTextNode(`#${tag}`), - ]); + const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]); tagEl.addEventListener("click", () => searchByTag(tag)); cloud.appendChild(tagEl); }); @@ -2541,7 +2597,7 @@ } function removeTagFilter(tag) { - selectedTags = selectedTags.filter(t => t !== tag); + selectedTags = selectedTags.filter((t) => t !== tag); if (selectedTags.length > 0) { performTagSearch(); } else { @@ -2580,20 +2636,21 @@ if (selectedTags.length > 0) { const activeTags = el("div", { class: "search-results-active-tags" }); selectedTags.forEach((tag) => { - const removeBtn = el("button", { - class: "search-results-active-tag-remove", - title: `Retirer ${tag} du filtre`, - "aria-label": `Retirer ${tag} du filtre` - }, [document.createTextNode("×")]); + const removeBtn = el( + "button", + { + class: "search-results-active-tag-remove", + title: `Retirer ${tag} du filtre`, + "aria-label": `Retirer ${tag} du filtre`, + }, + [document.createTextNode("×")], + ); removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeTagFilter(tag); }); - const chip = el("span", { class: "search-results-active-tag" }, [ - document.createTextNode(`#${tag}`), - removeBtn, - ]); + const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]); activeTags.appendChild(chip); }); header.appendChild(activeTags); @@ -2621,7 +2678,9 @@ try { const active = document.querySelector(selector); if (active) active.classList.add("active"); - } catch (e) { /* selector might fail on special chars */ } + } catch (e) { + /* selector might fail on special chars */ + } // Show loading state while fetching const area = document.getElementById("content-area"); @@ -2642,22 +2701,28 @@ // Breadcrumb const parts = data.path.split("/"); const breadcrumbEls = []; - breadcrumbEls.push(makeBreadcrumbSpan(data.vault, () => { - focusPathInSidebar(data.vault, "", { alignToTop: true }); - })); + breadcrumbEls.push( + makeBreadcrumbSpan(data.vault, () => { + focusPathInSidebar(data.vault, "", { alignToTop: true }); + }), + ); let accumulated = ""; parts.forEach((part, i) => { breadcrumbEls.push(el("span", { class: "sep" }, [document.createTextNode(" / ")])); accumulated += (accumulated ? "/" : "") + part; const p = accumulated; if (i < parts.length - 1) { - breadcrumbEls.push(makeBreadcrumbSpan(part, () => { - focusPathInSidebar(data.vault, p, { alignToTop: true }); - })); + breadcrumbEls.push( + makeBreadcrumbSpan(part, () => { + focusPathInSidebar(data.vault, p, { alignToTop: true }); + }), + ); } else { - breadcrumbEls.push(makeBreadcrumbSpan(part.replace(/\.md$/i, ""), () => { - focusPathInSidebar(data.vault, data.path, { alignToTop: false }); - })); + breadcrumbEls.push( + makeBreadcrumbSpan(part.replace(/\.md$/i, ""), () => { + focusPathInSidebar(data.vault, data.path, { alignToTop: false }); + }), + ); } }); @@ -2674,10 +2739,7 @@ }); // Action buttons - const copyBtn = el("button", { class: "btn-action", title: "Copier la source" }, [ - icon("copy", 14), - document.createTextNode("Copier"), - ]); + const copyBtn = el("button", { class: "btn-action", title: "Copier la source" }, [icon("copy", 14), document.createTextNode("Copier")]); copyBtn.addEventListener("click", async () => { try { // Fetch raw content if not already cached @@ -2695,15 +2757,9 @@ } }); - const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [ - icon("code", 14), - document.createTextNode("Source"), - ]); + const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [icon("code", 14), document.createTextNode("Source")]); - const downloadBtn = el("button", { class: "btn-action", title: "Télécharger" }, [ - icon("download", 14), - document.createTextNode("Télécharger"), - ]); + const downloadBtn = el("button", { class: "btn-action", title: "Télécharger" }, [icon("download", 14), document.createTextNode("Télécharger")]); downloadBtn.addEventListener("click", () => { const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`; const a = document.createElement("a"); @@ -2714,27 +2770,18 @@ document.body.removeChild(a); }); - const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [ - icon("edit", 14), - document.createTextNode("Éditer"), - ]); + const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [icon("edit", 14), document.createTextNode("Éditer")]); editBtn.addEventListener("click", () => { openEditor(data.vault, data.path); }); - const openNewWindowBtn = el("button", { class: "btn-action", title: "Ouvrir dans une nouvelle fenêtre" }, [ - icon("external-link", 14), - document.createTextNode("pop-out"), - ]); + const openNewWindowBtn = el("button", { class: "btn-action", title: "Ouvrir dans une nouvelle fenêtre" }, [icon("external-link", 14), document.createTextNode("pop-out")]); openNewWindowBtn.addEventListener("click", () => { const popoutUrl = `/popout/${encodeURIComponent(data.vault)}/${encodeURIComponent(data.path)}`; - window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); + window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, "_")}`, "width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no"); }); - const tocBtn = el("button", { class: "btn-action", id: "toc-toggle-btn", title: "Afficher/Masquer le sommaire" }, [ - icon("list", 14), - document.createTextNode("TOC"), - ]); + const tocBtn = el("button", { class: "btn-action", id: "toc-toggle-btn", title: "Afficher/Masquer le sommaire" }, [icon("list", 14), document.createTextNode("TOC")]); tocBtn.addEventListener("click", () => { RightSidebarManager.toggle(); }); @@ -2779,11 +2826,7 @@ // Assemble area.innerHTML = ""; area.appendChild(breadcrumb); - area.appendChild(el("div", { class: "file-header" }, [ - el("div", { class: "file-title" }, [document.createTextNode(data.title)]), - tagsDiv, - el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn, editBtn, openNewWindowBtn, tocBtn]), - ])); + area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn, editBtn, openNewWindowBtn, tocBtn])])); if (fmSection) area.appendChild(fmSection); area.appendChild(mdDiv); area.appendChild(rawDiv); @@ -2817,6 +2860,212 @@ let _recentTimestampTimer = null; let _recentFilesCache = []; + // --------------------------------------------------------------------------- + // Dashboard Recent Files Widget + // --------------------------------------------------------------------------- + const DashboardRecentWidget = { + _cache: [], + _currentFilter: "", + + async load(vaultFilter = "") { + this._currentFilter = vaultFilter; + this.showLoading(); + + let url = "/api/recent"; + if (vaultFilter) url += `?vault=${encodeURIComponent(vaultFilter)}`; + + try { + const data = await api(url); + this._cache = data.files || []; + this.render(); + } catch (err) { + console.error("Dashboard: Failed to load recent files:", err); + this.showError(); + } + }, + + showLoading() { + const grid = document.getElementById("dashboard-recent-grid"); + const loading = document.getElementById("dashboard-loading"); + const empty = document.getElementById("dashboard-recent-empty"); + const count = document.getElementById("dashboard-count"); + + if (grid) grid.innerHTML = ""; + if (loading) loading.classList.add("active"); + if (empty) empty.classList.add("hidden"); + if (count) count.textContent = ""; + }, + + render() { + const grid = document.getElementById("dashboard-recent-grid"); + const loading = document.getElementById("dashboard-loading"); + const empty = document.getElementById("dashboard-recent-empty"); + const count = document.getElementById("dashboard-count"); + + if (loading) loading.classList.remove("active"); + + if (!this._cache || this._cache.length === 0) { + this.showEmpty(); + return; + } + + if (empty) empty.classList.add("hidden"); + if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`; + + if (!grid) return; + grid.innerHTML = ""; + + this._cache.forEach((f, index) => { + const card = this._createCard(f, index); + grid.appendChild(card); + }); + + safeCreateIcons(); + }, + + _createCard(file, index) { + const card = document.createElement("div"); + card.className = "dashboard-card"; + card.setAttribute("data-vault", file.vault); + card.setAttribute("data-path", file.path); + card.style.animationDelay = `${Math.min(index * 50, 400)}ms`; + + // Header with icon and vault badge + const header = document.createElement("div"); + header.className = "dashboard-card-header"; + + const icon = document.createElement("div"); + icon.className = "dashboard-card-icon"; + icon.innerHTML = + ''; + + const badge = document.createElement("span"); + badge.className = "dashboard-vault-badge"; + badge.textContent = file.vault; + + header.appendChild(icon); + header.appendChild(badge); + card.appendChild(header); + + // Title + const title = document.createElement("h3"); + title.className = "dashboard-card-title"; + title.textContent = file.title || file.path.split("/").pop(); + title.title = file.title || file.path; + card.appendChild(title); + + // Path + const pathParts = file.path.split("/"); + if (pathParts.length > 1) { + const path = document.createElement("div"); + path.className = "dashboard-card-path"; + path.textContent = pathParts.slice(0, -1).join(" / "); + path.title = file.path; + card.appendChild(path); + } + + // Preview + if (file.preview) { + const preview = document.createElement("p"); + preview.className = "dashboard-card-preview"; + preview.textContent = file.preview; + card.appendChild(preview); + } + + // Footer with time and tags + const footer = document.createElement("div"); + footer.className = "dashboard-card-footer"; + + const time = document.createElement("span"); + time.className = "dashboard-card-time"; + time.innerHTML = ` ${file.mtime_human || this._humanizeDelta(file.mtime)}`; + + footer.appendChild(time); + + // Tags + if (file.tags && file.tags.length > 0) { + const tags = document.createElement("div"); + tags.className = "dashboard-card-tags"; + file.tags.slice(0, 3).forEach((tag) => { + const tagEl = document.createElement("span"); + tagEl.className = "tag-pill"; + tagEl.textContent = tag; + tags.appendChild(tagEl); + }); + footer.appendChild(tags); + } + + card.appendChild(footer); + + // Click handler + card.addEventListener("click", () => { + openFile(file.vault, file.path); + }); + + return card; + }, + + showEmpty() { + const grid = document.getElementById("dashboard-recent-grid"); + const loading = document.getElementById("dashboard-loading"); + const empty = document.getElementById("dashboard-recent-empty"); + const count = document.getElementById("dashboard-count"); + + if (grid) grid.innerHTML = ""; + if (loading) loading.classList.remove("active"); + if (empty) empty.classList.remove("hidden"); + if (count) count.textContent = "0 fichiers"; + safeCreateIcons(); + }, + + showError() { + this.showEmpty(); + const empty = document.getElementById("dashboard-recent-empty"); + if (empty) { + const msg = empty.querySelector("span"); + if (msg) msg.textContent = "Erreur de chargement"; + } + }, + + _humanizeDelta(mtime) { + const delta = Date.now() / 1000 - mtime; + if (delta < 60) return "à l'instant"; + if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`; + if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`; + if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`; + return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" }); + }, + + populateVaultFilter() { + const select = document.getElementById("dashboard-vault-filter"); + if (!select) return; + + // Keep first option "Tous les vaults" + while (select.options.length > 1) select.remove(1); + + if (typeof allVaults !== "undefined" && Array.isArray(allVaults)) { + allVaults.forEach((v) => { + const opt = document.createElement("option"); + opt.value = v.name; + opt.textContent = v.name; + select.appendChild(opt); + }); + } + }, + + init() { + const select = document.getElementById("dashboard-vault-filter"); + if (select) { + select.addEventListener("change", () => { + this.load(select.value || null); + }); + } + + this.populateVaultFilter(); + this.load(null); + }, + }; + async function loadRecentFiles(vaultFilter) { const listEl = document.getElementById("recent-list"); const emptyEl = document.getElementById("recent-empty"); @@ -2830,8 +3079,10 @@ renderRecentList(_recentFilesCache); } catch (err) { console.error("Failed to load recent files:", err); - listEl.innerHTML = ''; - if (emptyEl) { emptyEl.classList.remove("hidden"); } + listEl.innerHTML = ""; + if (emptyEl) { + emptyEl.classList.remove("hidden"); + } } } @@ -2842,7 +3093,10 @@ listEl.innerHTML = ""; if (!files || files.length === 0) { - if (emptyEl) { emptyEl.classList.remove("hidden"); safeCreateIcons(); } + if (emptyEl) { + emptyEl.classList.remove("hidden"); + safeCreateIcons(); + } return; } if (emptyEl) emptyEl.classList.add("hidden"); @@ -2852,35 +3106,26 @@ // Header row: time + vault badge const header = el("div", { class: "recent-item-header" }); - const timeSpan = el("span", { class: "recent-time" }, [ - icon("clock", 11), - document.createTextNode(f.mtime_human), - ]); + const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]); const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]); header.appendChild(timeSpan); header.appendChild(badge); item.appendChild(header); // Title - const titleEl = el("div", { class: "recent-item-title" }, [ - document.createTextNode(f.title || f.path.split("/").pop()), - ]); + const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]); item.appendChild(titleEl); // Path breadcrumb const pathParts = f.path.split("/"); if (pathParts.length > 1) { - const pathEl = el("div", { class: "recent-item-path" }, [ - document.createTextNode(pathParts.slice(0, -1).join(" / ")), - ]); + const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]); item.appendChild(pathEl); } // Preview if (f.preview) { - const previewEl = el("div", { class: "recent-item-preview" }, [ - document.createTextNode(f.preview), - ]); + const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]); item.appendChild(previewEl); } @@ -2905,7 +3150,7 @@ } function _humanizeDelta(mtime) { - const delta = (Date.now() / 1000) - mtime; + const delta = Date.now() / 1000 - mtime; if (delta < 60) return "à l'instant"; if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`; if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`; @@ -2979,9 +3224,7 @@ const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" }; filterInput.placeholder = placeholders[tab] || ""; } - const query = filterInput - ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) - : ""; + const query = filterInput ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : ""; if (query) { if (tab === "vaults") performTreeSearch(query); else if (tab === "tags") filterTagCloud(query); @@ -3024,11 +3267,11 @@ function initHelpNavigation() { const helpContent = document.querySelector(".help-content"); const navLinks = document.querySelectorAll(".help-nav-link"); - + if (!helpContent || !navLinks.length) return; // Handle nav link clicks - navLinks.forEach(link => { + navLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const targetId = link.getAttribute("href").substring(1); @@ -3040,26 +3283,29 @@ }); // Scroll spy - update active nav link based on scroll position - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const id = entry.target.getAttribute("id"); - navLinks.forEach(link => { - if (link.getAttribute("href") === `#${id}`) { - navLinks.forEach(l => l.classList.remove("active")); - link.classList.add("active"); - } - }); - } - }); - }, { - root: helpContent, - rootMargin: "-20% 0px -70% 0px", - threshold: 0 - }); + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const id = entry.target.getAttribute("id"); + navLinks.forEach((link) => { + if (link.getAttribute("href") === `#${id}`) { + navLinks.forEach((l) => l.classList.remove("active")); + link.classList.add("active"); + } + }); + } + }); + }, + { + root: helpContent, + rootMargin: "-20% 0px -70% 0px", + threshold: 0, + }, + ); // Observe all sections - document.querySelectorAll(".help-section").forEach(section => { + document.querySelectorAll(".help-section").forEach((section) => { observer.observe(section); }); } @@ -3075,7 +3321,7 @@ const modal = document.getElementById("config-modal"); const addBtn = document.getElementById("config-add-btn"); const patternInput = document.getElementById("config-pattern-input"); - + if (!openBtn || !closeBtn || !modal) return; openBtn.addEventListener("click", async () => { @@ -3149,16 +3395,27 @@ const _FRONTEND_CONFIG_KEY = "obsigate-perf-config"; function _getFrontendConfig() { - try { return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}"); } - catch { return {}; } + try { + return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}"); + } catch { + return {}; + } } function applyFrontendConfig() { const cfg = _getFrontendConfig(); - if (cfg.debounce_ms) { /* applied dynamically in debounce setTimeout */ } - if (cfg.results_per_page) { /* used as ADVANCED_SEARCH_LIMIT override */ } - if (cfg.min_query_length) { /* used as MIN_SEARCH_LENGTH override */ } - if (cfg.search_timeout_ms) { /* used as SEARCH_TIMEOUT_MS override */ } + if (cfg.debounce_ms) { + /* applied dynamically in debounce setTimeout */ + } + if (cfg.results_per_page) { + /* used as ADVANCED_SEARCH_LIMIT override */ + } + if (cfg.min_query_length) { + /* used as MIN_SEARCH_LENGTH override */ + } + if (cfg.search_timeout_ms) { + /* used as SEARCH_TIMEOUT_MS override */ + } } function _getEffective(key, fallback) { @@ -3259,7 +3516,10 @@ async function forceReindex() { const btn = document.getElementById("cfg-reindex"); - if (btn) { btn.disabled = true; btn.textContent = "Réindexation..."; } + if (btn) { + btn.disabled = true; + btn.textContent = "Réindexation..."; + } try { await api("/api/index/reload"); showToast("Réindexation terminée", "success"); @@ -3269,7 +3529,10 @@ console.error("Reindex error:", err); showToast("Erreur de réindexation", "error"); } finally { - if (btn) { btn.disabled = false; btn.textContent = "Forcer réindexation"; } + if (btn) { + btn.disabled = false; + btn.textContent = "Forcer réindexation"; + } } } @@ -3282,13 +3545,23 @@ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - search_workers: 2, debounce_ms: 300, results_per_page: 50, - min_query_length: 2, search_timeout_ms: 30000, max_content_size: 100000, - title_boost: 3.0, path_boost: 1.5, tag_boost: 2.0, prefix_max_expansions: 50, - snippet_context_chars: 120, max_snippet_highlights: 5, + search_workers: 2, + debounce_ms: 300, + results_per_page: 50, + min_query_length: 2, + search_timeout_ms: 30000, + max_content_size: 100000, + title_boost: 3.0, + path_boost: 1.5, + tag_boost: 2.0, + prefix_max_expansions: 50, + snippet_context_chars: 120, + max_snippet_highlights: 5, }), }); - } catch (err) { console.error("Reset config error:", err); } + } catch (err) { + console.error("Reset config error:", err); + } loadConfigFields(); showToast("Configuration réinitialisée", "success"); } @@ -3308,22 +3581,31 @@ function renderDiagnostics(container, data) { container.innerHTML = ""; const sections = [ - { title: "Index", rows: [ - ["Fichiers indexés", data.index.total_files], - ["Tags uniques", data.index.total_tags], - ["Vaults", Object.keys(data.index.vaults).join(", ")], - ]}, - { title: "Index inversé", rows: [ - ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()], - ["Postings total", data.inverted_index.total_postings.toLocaleString()], - ["Documents", data.inverted_index.documents], - ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"], - ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"], - ]}, - { title: "Moteur de recherche", rows: [ - ["Executor actif", data.search_executor.active ? "Oui" : "Non"], - ["Workers max", data.search_executor.max_workers], - ]}, + { + title: "Index", + rows: [ + ["Fichiers indexés", data.index.total_files], + ["Tags uniques", data.index.total_tags], + ["Vaults", Object.keys(data.index.vaults).join(", ")], + ], + }, + { + title: "Index inversé", + rows: [ + ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()], + ["Postings total", data.inverted_index.total_postings.toLocaleString()], + ["Documents", data.inverted_index.documents], + ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"], + ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"], + ], + }, + { + title: "Moteur de recherche", + rows: [ + ["Executor actif", data.search_executor.active ? "Oui" : "Non"], + ["Workers max", data.search_executor.max_workers], + ], + }, ]; sections.forEach((section) => { const div = document.createElement("div"); @@ -3343,13 +3625,13 @@ } // --- Hidden Files Configuration --- - + async function loadHiddenFilesSettings() { const container = document.getElementById("hidden-files-vault-list"); if (!container) return; - + container.innerHTML = '
Chargement...
'; - + try { const settings = await api("/api/vaults/settings/all"); renderHiddenFilesSettings(container, settings); @@ -3358,85 +3640,80 @@ container.innerHTML = '
Erreur de chargement
'; } } - + function renderHiddenFilesSettings(container, allSettings) { container.innerHTML = ""; - + if (!allVaults || allVaults.length === 0) { container.innerHTML = '
Aucun vault configuré
'; return; } - - allVaults.forEach(vault => { + + allVaults.forEach((vault) => { const settings = allSettings[vault.name] || { hideHiddenFiles: false }; - + const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name }); - + // Vault header - const header = el("div", { class: "hidden-files-vault-header" }, [ - el("h3", {}, [document.createTextNode(vault.name)]), - el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")]) - ]); - + const header = el("div", { class: "hidden-files-vault-header" }, [el("h3", {}, [document.createTextNode(vault.name)]), el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")])]); + // Hide hidden files toggle const toggleRow = el("div", { class: "config-row" }, [ - el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [ - document.createTextNode("Masquer les fichiers/dossiers cachés") - ]), + el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]), el("label", { class: "config-toggle" }, [ - el("input", { - type: "checkbox", + el("input", { + type: "checkbox", id: `hide-hidden-${vault.name}`, "data-vault": vault.name, - checked: settings.hideHiddenFiles ? "true" : false + checked: settings.hideHiddenFiles ? "true" : false, }), - el("span", { class: "config-toggle-slider" }) + el("span", { class: "config-toggle-slider" }), ]), - el("span", { class: "config-hint" }, [ - document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)") - ]) + el("span", { class: "config-hint" }, [document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)")]), ]); - + vaultCard.appendChild(header); vaultCard.appendChild(toggleRow); - + container.appendChild(vaultCard); }); } - - + async function saveHiddenFilesSettings() { const btn = document.getElementById("cfg-save-hidden-files"); - if (btn) { btn.disabled = true; btn.textContent = "Sauvegarde..."; } - + if (btn) { + btn.disabled = true; + btn.textContent = "Sauvegarde..."; + } + try { const vaultCards = document.querySelectorAll(".hidden-files-vault-card"); const promises = []; - - vaultCards.forEach(card => { + + vaultCards.forEach((card) => { const vaultName = card.dataset.vault; const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false; - + const settings = { - hideHiddenFiles + hideHiddenFiles, }; - + promises.push( api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(settings) - }) + body: JSON.stringify(settings), + }), ); }); - + await Promise.all(promises); - + // Reload vault settings to update the cache await loadVaultSettings(); - + showToast("✓ Paramètres sauvegardés", "success"); - + // Refresh the UI to apply the filter await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]); } catch (err) { @@ -3444,46 +3721,56 @@ const errorMsg = err.message || "Erreur inconnue"; showToast(`Erreur: ${errorMsg}`, "error"); } finally { - if (btn) { btn.disabled = false; btn.textContent = "💾 Sauvegarder"; } + if (btn) { + btn.disabled = false; + btn.textContent = "💾 Sauvegarder"; + } } } - function renderConfigFilters() { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; const container = document.getElementById("config-filters-list"); - + container.innerHTML = ""; - + filters.forEach((filter, index) => { const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [ el("span", {}, [document.createTextNode(filter.pattern)]), - el("button", { - class: "config-filter-toggle", - title: filter.enabled ? "Désactiver" : "Activer", - type: "button" - }, [document.createTextNode(filter.enabled ? "✓" : "○")]), - el("button", { - class: "config-filter-remove", - title: "Supprimer", - type: "button" - }, [document.createTextNode("×")]), + el( + "button", + { + class: "config-filter-toggle", + title: filter.enabled ? "Désactiver" : "Activer", + type: "button", + }, + [document.createTextNode(filter.enabled ? "✓" : "○")], + ), + el( + "button", + { + class: "config-filter-remove", + title: "Supprimer", + type: "button", + }, + [document.createTextNode("×")], + ), ]); - + const toggleBtn = badge.querySelector(".config-filter-toggle"); const removeBtn = badge.querySelector(".config-filter-remove"); - + toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); toggleConfigFilter(index); }); - + removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeConfigFilter(index); }); - + container.appendChild(badge); }); } @@ -3496,7 +3783,7 @@ config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); - refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err)); + refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } } @@ -3507,27 +3794,27 @@ config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); - refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err)); + refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } function addConfigFilter() { const input = document.getElementById("config-pattern-input"); const pattern = input.value.trim(); - + if (!pattern) return; - + const regex = TagFilterService.patternToRegex(pattern); const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; - + const newFilter = { pattern, regex, enabled: true }; filters.push(newFilter); config.tagFilters = filters; TagFilterService.saveConfig(config); - + input.value = ""; renderConfigFilters(); - refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err)); + refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); updateRegexPreview(); } @@ -3536,7 +3823,7 @@ const preview = document.getElementById("config-regex-preview"); const code = document.getElementById("config-regex-code"); const pattern = input.value.trim(); - + if (pattern) { const regex = TagFilterService.patternToRegex(pattern); code.textContent = `^${regex}$`; @@ -3571,18 +3858,21 @@ // Debounced search execution clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - const q = input.value.trim(); - const vault = document.getElementById("vault-filter").value; - const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; - advancedSearchOffset = 0; - if ((q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH)) || tagFilter) { - performAdvancedSearch(q, vault, tagFilter); - } else if (q.length === 0) { - SearchChips.clear(); - showWelcome(); - } - }, _getEffective("debounce_ms", 300)); + searchTimeout = setTimeout( + () => { + const q = input.value.trim(); + const vault = document.getElementById("vault-filter").value; + const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; + advancedSearchOffset = 0; + if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) { + performAdvancedSearch(q, vault, tagFilter); + } else if (q.length === 0) { + SearchChips.clear(); + showWelcome(); + } + }, + _getEffective("debounce_ms", 300), + ); }); // --- Focus handler: show history dropdown --- @@ -3724,9 +4014,12 @@ if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; // Search timeout — abort if server takes too long - const timeoutId = setTimeout(() => { - if (searchAbortController) searchAbortController.abort(); - }, _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS)); + const timeoutId = setTimeout( + () => { + if (searchAbortController) searchAbortController.abort(); + }, + _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS), + ); try { const data = await api(url, { signal: searchAbortController.signal }); @@ -3753,9 +4046,7 @@ const header = buildSearchResultsHeader(data, query, tagFilter); area.appendChild(header); if (data.results.length === 0) { - area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [ - document.createTextNode("Aucun résultat trouvé."), - ])); + area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")])); return; } const container = el("div", { class: "search-results" }); @@ -3764,7 +4055,7 @@ if (!shouldDisplayPath(r.path, r.vault)) { return; // Skip this result } - + const titleDiv = el("div", { class: "search-result-title" }); if (query && query.trim()) { highlightSearchText(titleDiv, r.title, query, searchCaseSensitive); @@ -3777,17 +4068,16 @@ } else { snippetDiv.textContent = r.snippet || ""; } - const item = el("div", { class: "search-result-item" }, [ - titleDiv, - el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), - snippetDiv, - ]); + const item = el("div", { class: "search-result-item" }, [titleDiv, el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), snippetDiv]); if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { if (!TagFilterService.isTagFiltered(tag)) { const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); - tagEl.addEventListener("click", (e) => { e.stopPropagation(); addTagFilter(tag); }); + tagEl.addEventListener("click", (e) => { + e.stopPropagation(); + addTagFilter(tag); + }); tagsDiv.appendChild(tagEl); } }); @@ -3853,14 +4143,19 @@ if (selectedTags.length > 0) { const activeTags = el("div", { class: "search-results-active-tags" }); selectedTags.forEach((tag) => { - const removeBtn = el("button", { - class: "search-results-active-tag-remove", - title: `Retirer ${tag} du filtre`, - }, [document.createTextNode("×")]); - removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeTagFilter(tag); }); - const chip = el("span", { class: "search-results-active-tag" }, [ - document.createTextNode(`#${tag}`), removeBtn, - ]); + const removeBtn = el( + "button", + { + class: "search-results-active-tag-remove", + title: `Retirer ${tag} du filtre`, + }, + [document.createTextNode("×")], + ); + removeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + removeTagFilter(tag); + }); + const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]); activeTags.appendChild(chip); }); area.appendChild(activeTags); @@ -3916,9 +4211,7 @@ // Empty state if (data.results.length === 0) { - area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [ - document.createTextNode("Aucun résultat trouvé."), - ])); + area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")])); return; } @@ -3929,7 +4222,7 @@ if (!shouldDisplayPath(r.path, r.vault)) { return; // Skip this result } - + const titleDiv = el("div", { class: "search-result-title" }); if (freeText) { highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive); @@ -3951,10 +4244,7 @@ const scoreEl = el("span", { class: "search-result-score", style: "font-size:0.7rem;color:var(--text-muted);margin-left:8px" }); scoreEl.textContent = `score: ${r.score}`; - const vaultPath = el("div", { class: "search-result-vault" }, [ - document.createTextNode(r.vault + " / " + r.path), - scoreEl, - ]); + const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]); const item = el("div", { class: "search-result-item" }, [titleDiv, vaultPath, snippetDiv]); @@ -3963,7 +4253,10 @@ r.tags.forEach((tag) => { if (!TagFilterService.isTagFiltered(tag)) { const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); - tagEl.addEventListener("click", (e) => { e.stopPropagation(); addTagFilter(tag); }); + tagEl.addEventListener("click", (e) => { + e.stopPropagation(); + addTagFilter(tag); + }); tagsDiv.appendChild(tagEl); } }); @@ -4098,11 +4391,11 @@ // --------------------------------------------------------------------------- // Frontmatter Accent Card Builder // --------------------------------------------------------------------------- - + function buildFrontmatterCard(frontmatter) { // Helper: format date function formatDate(iso) { - if (!iso) return '—'; + if (!iso) return "—"; const d = new Date(iso); const date = d.toISOString().slice(0, 10); const time = d.toTimeString().slice(0, 5); @@ -4110,8 +4403,7 @@ } // Extract boolean flags - const booleanFlags = ['publish', 'favoris', 'template', 'task', 'archive', 'draft', 'private'] - .map(key => ({ key, value: !!frontmatter[key] })); + const booleanFlags = ["publish", "favoris", "template", "task", "archive", "draft", "private"].map((key) => ({ key, value: !!frontmatter[key] })); // Toggle state let isOpen = true; @@ -4119,98 +4411,56 @@ // Build header with chevron const chevron = el("span", { class: "fm-chevron open" }); chevron.innerHTML = ''; - - const fmHeader = el("div", { class: "fm-header" }, [ - chevron, - document.createTextNode("Frontmatter") - ]); + + const fmHeader = el("div", { class: "fm-header" }, [chevron, document.createTextNode("Frontmatter")]); // ZONE 1: Top strip const topBadges = []; - + // Title badge - const title = frontmatter.titre || frontmatter.title || ''; + const title = frontmatter.titre || frontmatter.title || ""; if (title) { topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)])); } // Status badge if (frontmatter.statut) { - const statusBadge = el("span", { class: "ac-badge green" }, [ - el("span", { class: "ac-dot" }), - document.createTextNode(frontmatter.statut) - ]); + const statusBadge = el("span", { class: "ac-badge green" }, [el("span", { class: "ac-dot" }), document.createTextNode(frontmatter.statut)]); topBadges.push(statusBadge); } // Category badge if (frontmatter.catégorie || frontmatter.categorie) { const cat = frontmatter.catégorie || frontmatter.categorie; - const catBadge = el("span", { class: "ac-badge blue" }, [ - document.createTextNode(cat) - ]); + const catBadge = el("span", { class: "ac-badge blue" }, [document.createTextNode(cat)]); topBadges.push(catBadge); } // Publish badge if (frontmatter.publish) { - topBadges.push(el("span", { class: "ac-badge purple" }, [ - document.createTextNode("publié") - ])); + topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("publié")])); } // Favoris badge if (frontmatter.favoris) { - topBadges.push(el("span", { class: "ac-badge purple" }, [ - document.createTextNode("favori") - ])); + topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("favori")])); } const acTop = el("div", { class: "ac-top" }, topBadges); // ZONE 2: Body 2 columns const leftCol = el("div", { class: "ac-col" }, [ - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), - el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || '—')]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), - el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || '—')]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("statut")]), - el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || '—')]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), - el("span", { class: "ac-v muted" }, [ - document.createTextNode( - frontmatter.aliases && frontmatter.aliases.length > 0 - ? frontmatter.aliases.join(', ') - : '[]' - ) - ]) - ]) + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || "—")])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || "—")])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("statut")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || "—")])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), el("span", { class: "ac-v muted" }, [document.createTextNode(frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(", ") : "[]")])]), ]); const rightCol = el("div", { class: "ac-col" }, [ - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), - el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), - el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("publish")]), - el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))]) - ]), - el("div", { class: "ac-row" }, [ - el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), - el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))]) - ]) + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("publish")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])]), ]); const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]); @@ -4218,38 +4468,24 @@ // ZONE 3: Tags row const tagPills = []; if (frontmatter.tags && frontmatter.tags.length > 0) { - frontmatter.tags.forEach(tag => { + frontmatter.tags.forEach((tag) => { tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)])); }); } - const acTagsRow = el("div", { class: "ac-tags-row" }, [ - el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), - el("div", { class: "ac-tags-wrap" }, tagPills) - ]); + const acTagsRow = el("div", { class: "ac-tags-row" }, [el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), el("div", { class: "ac-tags-wrap" }, tagPills)]); // ZONE 4: Flags row const flagChips = []; - booleanFlags.forEach(flag => { + booleanFlags.forEach((flag) => { const chipClass = flag.value ? "flag-chip on" : "flag-chip off"; - flagChips.push(el("span", { class: chipClass }, [ - el("span", { class: "flag-dot" }), - document.createTextNode(flag.key) - ])); + flagChips.push(el("span", { class: chipClass }, [el("span", { class: "flag-dot" }), document.createTextNode(flag.key)])); }); - const acFlagsRow = el("div", { class: "ac-flags-row" }, [ - el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), - ...flagChips - ]); + const acFlagsRow = el("div", { class: "ac-flags-row" }, [el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), ...flagChips]); // Assemble the card - const acCard = el("div", { class: "ac-card" }, [ - acTop, - acBody, - acTagsRow, - acFlagsRow - ]); + const acCard = el("div", { class: "ac-card" }, [acTop, acBody, acTagsRow, acFlagsRow]); // Toggle functionality fmHeader.addEventListener("click", () => { @@ -4267,10 +4503,7 @@ }); // Wrap in section - const fmSection = el("div", { class: "fm-section" }, [ - fmHeader, - acCard - ]); + const fmSection = el("div", { class: "fm-section" }, [fmHeader, acCard]); return fmSection; } @@ -4290,7 +4523,9 @@ }); } if (children) { - children.forEach((c) => { if (c) e.appendChild(c); }); + children.forEach((c) => { + if (c) e.appendChild(c); + }); } return e; } @@ -4334,7 +4569,7 @@ svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.classList.add("icon"); - + const path1 = document.createElementNS(svgNS, "path"); path1.setAttribute("d", "M6 3h12l4 6-10 12L2 9z"); const path2 = document.createElementNS(svgNS, "path"); @@ -4343,7 +4578,7 @@ path3.setAttribute("d", "M12 21l4-12-3-6"); const path4 = document.createElementNS(svgNS, "path"); path4.setAttribute("d", "M2 9h20"); - + svg.appendChild(path1); svg.appendChild(path2); svg.appendChild(path3); @@ -4380,9 +4615,7 @@ if (index > start) { container.appendChild(document.createTextNode(text.slice(start, index))); } - const mark = el("mark", { class: "filter-highlight" }, [ - document.createTextNode(text.slice(index, index + query.length)), - ]); + const mark = el("mark", { class: "filter-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]); container.appendChild(mark); start = index + query.length; index = source.indexOf(needle, start); @@ -4414,9 +4647,7 @@ 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)), - ]); + 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); @@ -4429,14 +4660,12 @@ function showWelcome() { hideProgressBar(); - const area = document.getElementById("content-area"); - area.innerHTML = ` -
- -

ObsiGate

-

Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.

-
`; - safeCreateIcons(); + // Show the dashboard widget instead of simple welcome message + if (typeof DashboardRecentWidget !== "undefined") { + // Re-init the widget to refresh data and populate vault filter + DashboardRecentWidget.populateVaultFilter(); + DashboardRecentWidget.load(DashboardRecentWidget._currentFilter || null); + } } function showLoading() { @@ -4462,14 +4691,14 @@ function goHome() { const searchInput = document.getElementById("search-input"); if (searchInput) searchInput.value = ""; - + document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active")); - + currentVault = null; currentPath = null; showingSource = false; cachedRawSource = null; - + closeMobileSidebar(); showWelcome(); } @@ -4506,51 +4735,53 @@ const currentTheme = document.documentElement.getAttribute("data-theme"); const fileExt = filePath.split(".").pop().toLowerCase(); - + const extensions = [ basicSetup, - keymap.of([{ - key: "Mod-s", - run: () => { - saveFile(); - return true; - } - }]), + keymap.of([ + { + key: "Mod-s", + run: () => { + saveFile(); + return true; + }, + }, + ]), EditorView.lineWrapping, ]; // Add language support based on file extension const langMap = { - "md": markdown, - "markdown": markdown, - "py": python, - "js": javascript, - "jsx": javascript, - "ts": javascript, - "tsx": javascript, - "mjs": javascript, - "cjs": javascript, - "html": html, - "htm": html, - "css": css, - "scss": css, - "less": css, - "json": json, - "xml": xml, - "svg": xml, - "sql": sql, - "php": php, - "cpp": cpp, - "cc": cpp, - "cxx": cpp, - "c": cpp, - "h": cpp, - "hpp": cpp, - "java": java, - "rs": rust, - "sh": javascript, // Using javascript for shell scripts as fallback - "bash": javascript, - "zsh": javascript, + md: markdown, + markdown: markdown, + py: python, + js: javascript, + jsx: javascript, + ts: javascript, + tsx: javascript, + mjs: javascript, + cjs: javascript, + html: html, + htm: html, + css: css, + scss: css, + less: css, + json: json, + xml: xml, + svg: xml, + sql: sql, + php: php, + cpp: cpp, + cc: cpp, + cxx: cpp, + c: cpp, + h: cpp, + hpp: cpp, + java: java, + rs: rust, + sh: javascript, // Using javascript for shell scripts as fallback + bash: javascript, + zsh: javascript, }; const langMode = langMap[fileExt]; @@ -4586,7 +4817,7 @@ async function waitForCodeMirror() { let attempts = 0; while (!window.CodeMirror && attempts < 50) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } if (!window.CodeMirror) { @@ -4618,14 +4849,11 @@ saveBtn.innerHTML = ''; safeCreateIcons(); - const response = await fetch( - `/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, - { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content }), - } - ); + const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); if (!response.ok) { const error = await response.json(); @@ -4661,10 +4889,7 @@ deleteBtn.innerHTML = ''; safeCreateIcons(); - const response = await fetch( - `/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`, - { method: "DELETE" } - ); + const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`, { method: "DELETE" }); if (!response.ok) { const error = await response.json(); @@ -4709,15 +4934,19 @@ }); // Fix mouse wheel scrolling in editor - modal.addEventListener("wheel", (e) => { - const editorBody = document.getElementById("editor-body"); - if (editorBody && editorBody.contains(e.target)) { - // Let the editor handle the scroll - return; - } - // Prevent modal from scrolling if not in editor area - e.preventDefault(); - }, { passive: false }); + modal.addEventListener( + "wheel", + (e) => { + const editorBody = document.getElementById("editor-body"); + if (editorBody && editorBody.contains(e.target)) { + // Let the editor handle the scroll + return; + } + // Prevent modal from scrolling if not in editor area + e.preventDefault(); + }, + { passive: false }, + ); } // --------------------------------------------------------------------------- @@ -4868,16 +5097,13 @@ showToast(`${n} fichier(s) mis à jour (${vaults})`, "info"); // Refresh sidebar and tags if affected vault matches current context - const affectsCurrentVault = selectedContextVault === "all" || - (data.vaults || []).includes(selectedContextVault); + const affectsCurrentVault = selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault); if (affectsCurrentVault) { try { await Promise.all([loadVaults(), loadTags()]); // Refresh current file if it was updated if (currentVault && currentPath) { - const changed = (data.changes || []).some( - (c) => c.vault === currentVault && c.path === currentPath - ); + const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath); if (changed) { openFile(currentVault, currentPath); } @@ -5024,7 +5250,7 @@ index_complete: "Indexation tech.", }; const label = typeLabels[ev.type] || ev.type; - let detail = ev.data.vaults ? ev.data.vaults.join(", ") : (ev.data.vault || ""); + let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || ""; if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`; if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`; if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`; @@ -5045,43 +5271,43 @@ // --------------------------------------------------------------------------- const FindInPageManager = { isOpen: false, - searchTerm: '', + searchTerm: "", matches: [], currentIndex: -1, options: { caseSensitive: false, wholeWord: false, - useRegex: false + useRegex: false, }, debounceTimer: null, previousFocus: null, init() { - const bar = document.getElementById('find-in-page-bar'); - const input = document.getElementById('find-input'); - const prevBtn = document.getElementById('find-prev'); - const nextBtn = document.getElementById('find-next'); - const closeBtn = document.getElementById('find-close'); - const caseSensitiveBtn = document.getElementById('find-case-sensitive'); - const wholeWordBtn = document.getElementById('find-whole-word'); - const regexBtn = document.getElementById('find-regex'); + const bar = document.getElementById("find-in-page-bar"); + const input = document.getElementById("find-input"); + const prevBtn = document.getElementById("find-prev"); + const nextBtn = document.getElementById("find-next"); + const closeBtn = document.getElementById("find-close"); + const caseSensitiveBtn = document.getElementById("find-case-sensitive"); + const wholeWordBtn = document.getElementById("find-whole-word"); + const regexBtn = document.getElementById("find-regex"); if (!bar || !input) return; // Keyboard shortcuts - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Ctrl+F or Cmd+F to open - if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + if ((e.ctrlKey || e.metaKey) && e.key === "f") { e.preventDefault(); this.open(); } // Escape to close - if (e.key === 'Escape' && this.isOpen) { + if (e.key === "Escape" && this.isOpen) { e.preventDefault(); this.close(); } // Enter to go to next - if (e.key === 'Enter' && this.isOpen && document.activeElement === input) { + if (e.key === "Enter" && this.isOpen && document.activeElement === input) { e.preventDefault(); if (e.shiftKey) { this.goToPrevious(); @@ -5090,7 +5316,7 @@ } } // F3 for next/previous - if (e.key === 'F3' && this.isOpen) { + if (e.key === "F3" && this.isOpen) { e.preventDefault(); if (e.shiftKey) { this.goToPrevious(); @@ -5101,7 +5327,7 @@ }); // Input event with debounce - input.addEventListener('input', (e) => { + input.addEventListener("input", (e) => { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { this.search(e.target.value); @@ -5109,30 +5335,30 @@ }); // Navigation buttons - prevBtn.addEventListener('click', () => this.goToPrevious()); - nextBtn.addEventListener('click', () => this.goToNext()); + prevBtn.addEventListener("click", () => this.goToPrevious()); + nextBtn.addEventListener("click", () => this.goToNext()); // Close button - closeBtn.addEventListener('click', () => this.close()); + closeBtn.addEventListener("click", () => this.close()); // Option toggles - caseSensitiveBtn.addEventListener('click', () => { + caseSensitiveBtn.addEventListener("click", () => { this.options.caseSensitive = !this.options.caseSensitive; - caseSensitiveBtn.setAttribute('aria-pressed', this.options.caseSensitive); + caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); - wholeWordBtn.addEventListener('click', () => { + wholeWordBtn.addEventListener("click", () => { this.options.wholeWord = !this.options.wholeWord; - wholeWordBtn.setAttribute('aria-pressed', this.options.wholeWord); + wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); - regexBtn.addEventListener('click', () => { + regexBtn.addEventListener("click", () => { this.options.useRegex = !this.options.useRegex; - regexBtn.setAttribute('aria-pressed', this.options.useRegex); + regexBtn.setAttribute("aria-pressed", this.options.useRegex); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); @@ -5142,8 +5368,8 @@ }, open() { - const bar = document.getElementById('find-in-page-bar'); - const input = document.getElementById('find-input'); + const bar = document.getElementById("find-in-page-bar"); + const input = document.getElementById("find-input"); if (!bar || !input) return; this.previousFocus = document.activeElement; @@ -5155,7 +5381,7 @@ }, close() { - const bar = document.getElementById('find-in-page-bar'); + const bar = document.getElementById("find-in-page-bar"); if (!bar) return; this.isOpen = false; @@ -5163,7 +5389,7 @@ this.clearHighlights(); this.matches = []; this.currentIndex = -1; - this.searchTerm = ''; + this.searchTerm = ""; // Restore previous focus if (this.previousFocus && this.previousFocus.focus) { @@ -5182,7 +5408,7 @@ return; } - const contentArea = document.querySelector('.md-content'); + const contentArea = document.querySelector(".md-content"); if (!contentArea) { this.updateCounter(); this.updateNavButtons(); @@ -5215,41 +5441,37 @@ if (!this.options.useRegex) { // Escape special regex characters - pattern = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + pattern = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } if (this.options.wholeWord) { - pattern = '\\b' + pattern + '\\b'; + pattern = "\\b" + pattern + "\\b"; } - const flags = this.options.caseSensitive ? 'g' : 'gi'; + const flags = this.options.caseSensitive ? "g" : "gi"; return new RegExp(pattern, flags); }, findMatches(container, regex) { - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - { - acceptNode: (node) => { - // Skip code blocks, scripts, styles - const parent = node.parentElement; - if (!parent) return NodeFilter.FILTER_REJECT; - const tagName = parent.tagName.toLowerCase(); - if (['code', 'pre', 'script', 'style'].includes(tagName)) { - return NodeFilter.FILTER_REJECT; - } - // Skip empty text nodes - if (!node.textContent || node.textContent.trim().length === 0) { - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { + acceptNode: (node) => { + // Skip code blocks, scripts, styles + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + const tagName = parent.tagName.toLowerCase(); + if (["code", "pre", "script", "style"].includes(tagName)) { + return NodeFilter.FILTER_REJECT; } - } - ); + // Skip empty text nodes + if (!node.textContent || node.textContent.trim().length === 0) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + }); let node; - while (node = walker.nextNode()) { + while ((node = walker.nextNode())) { const text = node.textContent; let match; regex.lastIndex = 0; // Reset regex @@ -5259,7 +5481,7 @@ node: node, index: match.index, length: match[0].length, - text: match[0] + text: match[0], }); // Prevent infinite loop with zero-width matches @@ -5283,7 +5505,7 @@ matchesByNode.forEach((entries, node) => { if (!node || !node.parentNode) return; - const text = node.textContent || ''; + const text = node.textContent || ""; let cursor = 0; const fragment = document.createDocumentFragment(); @@ -5295,10 +5517,10 @@ } const matchText = text.substring(match.index, match.index + match.length); - const mark = document.createElement('mark'); - mark.className = idx === this.currentIndex ? 'find-highlight find-highlight-active' : 'find-highlight'; + const mark = document.createElement("mark"); + mark.className = idx === this.currentIndex ? "find-highlight find-highlight-active" : "find-highlight"; mark.textContent = matchText; - mark.setAttribute('data-find-index', idx); + mark.setAttribute("data-find-index", idx); fragment.appendChild(mark); match.element = mark; @@ -5314,11 +5536,11 @@ }, clearHighlights() { - const contentArea = document.querySelector('.md-content'); + const contentArea = document.querySelector(".md-content"); if (!contentArea) return; - const marks = contentArea.querySelectorAll('mark.find-highlight'); - marks.forEach(mark => { + const marks = contentArea.querySelectorAll("mark.find-highlight"); + marks.forEach((mark) => { if (!mark.parentNode) return; const text = mark.textContent; const textNode = document.createTextNode(text); @@ -5334,7 +5556,7 @@ // Remove active class from current if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.remove('find-highlight-active'); + this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); } // Move to next (with wrapping) @@ -5342,7 +5564,7 @@ // Add active class to new current if (this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.add('find-highlight-active'); + this.matches[this.currentIndex].element.classList.add("find-highlight-active"); } this.scrollToMatch(this.currentIndex); @@ -5354,7 +5576,7 @@ // Remove active class from current if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.remove('find-highlight-active'); + this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); } // Move to previous (with wrapping) @@ -5362,7 +5584,7 @@ // Add active class to new current if (this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.add('find-highlight-active'); + this.matches[this.currentIndex].element.classList.add("find-highlight-active"); } this.scrollToMatch(this.currentIndex); @@ -5375,9 +5597,9 @@ const match = this.matches[index]; if (!match.element) return; - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) { - match.element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + match.element.scrollIntoView({ behavior: "smooth", block: "center" }); return; } @@ -5387,27 +5609,27 @@ contentArea.scrollTo({ top: elementTop - offset, - behavior: 'smooth' + behavior: "smooth", }); }, updateCounter() { - const counter = document.getElementById('find-counter'); + const counter = document.getElementById("find-counter"); if (!counter) return; const count = this.matches.length; if (count === 0) { - counter.textContent = '0 occurrence'; + counter.textContent = "0 occurrence"; } else if (count === 1) { - counter.textContent = '1 occurrence'; + counter.textContent = "1 occurrence"; } else { counter.textContent = `${count} occurrences`; } }, updateNavButtons() { - const prevBtn = document.getElementById('find-prev'); - const nextBtn = document.getElementById('find-next'); + const prevBtn = document.getElementById("find-prev"); + const nextBtn = document.getElementById("find-next"); if (!prevBtn || !nextBtn) return; const hasMatches = this.matches.length > 0; @@ -5416,7 +5638,7 @@ }, showError(message) { - const errorEl = document.getElementById('find-error'); + const errorEl = document.getElementById("find-error"); if (!errorEl) return; errorEl.textContent = message; @@ -5424,7 +5646,7 @@ }, hideError() { - const errorEl = document.getElementById('find-error'); + const errorEl = document.getElementById("find-error"); if (!errorEl) return; errorEl.hidden = true; @@ -5433,9 +5655,9 @@ saveState() { try { const state = { - options: this.options + options: this.options, }; - localStorage.setItem('obsigate-find-in-page-state', JSON.stringify(state)); + localStorage.setItem("obsigate-find-in-page-state", JSON.stringify(state)); } catch (e) { // Ignore localStorage errors } @@ -5443,26 +5665,26 @@ loadState() { try { - const saved = localStorage.getItem('obsigate-find-in-page-state'); + const saved = localStorage.getItem("obsigate-find-in-page-state"); if (saved) { const state = JSON.parse(saved); if (state.options) { this.options = { ...this.options, ...state.options }; // Update button states - const caseSensitiveBtn = document.getElementById('find-case-sensitive'); - const wholeWordBtn = document.getElementById('find-whole-word'); - const regexBtn = document.getElementById('find-regex'); + const caseSensitiveBtn = document.getElementById("find-case-sensitive"); + const wholeWordBtn = document.getElementById("find-whole-word"); + const regexBtn = document.getElementById("find-regex"); - if (caseSensitiveBtn) caseSensitiveBtn.setAttribute('aria-pressed', this.options.caseSensitive); - if (wholeWordBtn) wholeWordBtn.setAttribute('aria-pressed', this.options.wholeWord); - if (regexBtn) regexBtn.setAttribute('aria-pressed', this.options.useRegex); + if (caseSensitiveBtn) caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); + if (wholeWordBtn) wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); + if (regexBtn) regexBtn.setAttribute("aria-pressed", this.options.useRegex); } } } catch (e) { // Ignore localStorage errors } - } + }, }; // --------------------------------------------------------------------------- @@ -5496,7 +5718,7 @@ if (authOk) { try { await Promise.all([loadVaultSettings(), loadVaults(), loadTags()]); - + // Check for popup mode query parameter const urlParams = new URLSearchParams(window.location.search); if (urlParams.get("popup") === "true") { @@ -5526,22 +5748,23 @@ // PWA Service Worker Registration // --------------------------------------------------------------------------- function registerServiceWorker() { - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js') + if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("/sw.js") .then((registration) => { - console.log('[PWA] Service Worker registered successfully:', registration.scope); - + console.log("[PWA] Service Worker registered successfully:", registration.scope); + // Check for updates periodically setInterval(() => { registration.update(); }, 60000); // Check every minute - + // Handle service worker updates - registration.addEventListener('updatefound', () => { + registration.addEventListener("updatefound", () => { const newWorker = registration.installing; - newWorker.addEventListener('statechange', () => { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + newWorker.addEventListener("statechange", () => { + if (newWorker.state === "installed" && navigator.serviceWorker.controller) { // New service worker available showUpdateNotification(); } @@ -5549,15 +5772,15 @@ }); }) .catch((error) => { - console.log('[PWA] Service Worker registration failed:', error); + console.log("[PWA] Service Worker registration failed:", error); }); }); } } function showUpdateNotification() { - const message = document.createElement('div'); - message.className = 'pwa-update-notification'; + const message = document.createElement("div"); + message.className = "pwa-update-notification"; message.innerHTML = `
Une nouvelle version d'ObsiGate est disponible ! @@ -5566,7 +5789,7 @@
`; document.body.appendChild(message); - + // Auto-dismiss after 30 seconds setTimeout(() => { if (message.parentElement) { @@ -5577,34 +5800,38 @@ // Handle install prompt let deferredPrompt; - window.addEventListener('beforeinstallprompt', (e) => { + window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); deferredPrompt = e; - + // Show install button if desired - const installBtn = document.getElementById('pwa-install-btn'); + const installBtn = document.getElementById("pwa-install-btn"); if (installBtn) { - installBtn.style.display = 'block'; - installBtn.addEventListener('click', async () => { + installBtn.style.display = "block"; + installBtn.addEventListener("click", async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log(`[PWA] User response to install prompt: ${outcome}`); deferredPrompt = null; - installBtn.style.display = 'none'; + installBtn.style.display = "none"; } }); } }); // Log when app is installed - window.addEventListener('appinstalled', () => { - console.log('[PWA] ObsiGate has been installed'); - showToast('ObsiGate installé avec succès !', 'success'); + window.addEventListener("appinstalled", () => { + console.log("[PWA] ObsiGate has been installed"); + showToast("ObsiGate installé avec succès !", "success"); }); document.addEventListener("DOMContentLoaded", () => { init(); registerServiceWorker(); + // Initialize the dashboard recent files widget + if (typeof DashboardRecentWidget !== "undefined") { + DashboardRecentWidget.init(); + } }); })(); diff --git a/frontend/index.html b/frontend/index.html index 94e8af8..dc122b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -350,10 +350,33 @@
-
- -

ObsiGate

-

Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.

+
+
+
+ +

Derniers fichiers ouverts

+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/style.css b/frontend/style.css index 07fbf9f..6d401df 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1,7 +1,9 @@ -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,400;0,600;1,400&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,400;0,600;1,400&display=swap"); /* ===== RESET ===== */ -*, *::before, *::after { +*, +*::before, +*::after { margin: 0; padding: 0; box-sizing: border-box; @@ -25,12 +27,12 @@ --search-bg: #21262d; --scrollbar: #30363d; --resize-handle: #30363d; - --overlay-bg: rgba(0,0,0,0.5); + --overlay-bg: rgba(0, 0, 0, 0.5); --danger: #ff7b72; --danger-bg: #3d1a18; --success: #3fb950; --success-bg: #1a3d1f; - + /* Accent Card variables */ --accent-card: #58a6ff; --accent-bg: #1f2a3a; @@ -46,7 +48,7 @@ --text: #e6edf3; --text-2: #8b949e; --text-3: #484f58; - --mono: 'JetBrains Mono', monospace; + --mono: "JetBrains Mono", monospace; --radius: 6px; --radius-lg: 10px; } @@ -69,12 +71,12 @@ --search-bg: #ffffff; --scrollbar: #d0d7de; --resize-handle: #d0d7de; - --overlay-bg: rgba(0,0,0,0.3); + --overlay-bg: rgba(0, 0, 0, 0.3); --danger: #cf222e; --danger-bg: #ffebe9; --success: #1a7f37; --success-bg: #dafbe1; - + /* Accent Card variables */ --accent-card: #4f6ef7; --accent-bg: #eef1fe; @@ -86,11 +88,11 @@ --surface: #ffffff; --surface2: #f2f1ef; --surface3: #ebe9e6; - --border-md: rgba(0,0,0,0.13); + --border-md: rgba(0, 0, 0, 0.13); --text: #1a1917; --text-2: #6b6a67; --text-3: #a09e9b; - --mono: 'JetBrains Mono', monospace; + --mono: "JetBrains Mono", monospace; --radius: 6px; --radius-lg: 10px; } @@ -102,11 +104,13 @@ html { } body { - font-family: 'Lora', Georgia, serif; + font-family: "Lora", Georgia, serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.7; - transition: background 200ms ease, color 200ms ease; + transition: + background 200ms ease, + color 200ms ease; overflow: hidden; height: 100vh; } @@ -170,7 +174,7 @@ a:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--border)); border-radius: 8px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; font-weight: 600; color: var(--accent); @@ -220,7 +224,7 @@ a:hover { } .header-logo { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-weight: 700; font-size: 1.15rem; color: var(--accent); @@ -269,10 +273,12 @@ a:hover { border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.9rem; outline: none; - transition: border-color 200ms ease, background 200ms ease; + transition: + border-color 200ms ease, + background 200ms ease; } .search-input-wrapper input:focus { @@ -300,7 +306,7 @@ a:hover { background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; font-weight: 600; transition: all 150ms ease; @@ -334,7 +340,11 @@ a:hover { display: flex; align-items: center; justify-content: center; - transition: color 200ms ease, border-color 200ms ease, background 200ms ease, transform 180ms ease; + transition: + color 200ms ease, + border-color 200ms ease, + background 200ms ease, + transform 180ms ease; } .header-menu-btn:hover { color: var(--accent); @@ -355,7 +365,7 @@ a:hover { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 16px; - box-shadow: 0 16px 36px rgba(0,0,0,0.28); + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28); min-width: 260px; z-index: 100; padding: 8px; @@ -384,7 +394,9 @@ a:hover { border: none; background: transparent; cursor: pointer; - transition: background 160ms ease, color 160ms ease; + transition: + background 160ms ease, + color 160ms ease; } .menu-list-button:hover, @@ -415,7 +427,7 @@ a:hover { } .menu-list-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; font-weight: 600; color: var(--text-primary); @@ -433,7 +445,7 @@ a:hover { border-radius: 0; background: inherit; color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.78rem; font-weight: 500; outline: none; @@ -456,7 +468,8 @@ a:hover { background-color: #1e1e1e; color: #e0e0e0; } -.menu-select:hover, .menu-select:focus { +.menu-select:hover, +.menu-select:focus { color: var(--accent); } @@ -498,10 +511,12 @@ select { border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; cursor: pointer; - transition: border-color 200ms ease, background 200ms ease; + transition: + border-color 200ms ease, + background 200ms ease; } .custom-dropdown-trigger:hover { @@ -530,7 +545,7 @@ select { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; - box-shadow: 0 8px 24px rgba(0,0,0,0.4); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); max-height: 240px; overflow-y: auto; z-index: 1000; @@ -547,10 +562,12 @@ select { padding: 6px 10px; border-radius: 4px; cursor: pointer; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; color: var(--text-primary); - transition: background 150ms ease, color 150ms ease; + transition: + background 150ms ease, + color 150ms ease; } .custom-dropdown-option:hover, @@ -626,7 +643,9 @@ select { display: flex; flex-direction: column; overflow: hidden; - transition: background 200ms ease, transform 300ms ease; + transition: + background 200ms ease, + transform 300ms ease; flex-shrink: 0; } @@ -637,7 +656,6 @@ select { border-right: none; } - /* --- Sidebar filter --- */ .sidebar-filter { padding: 10px 12px; @@ -662,7 +680,7 @@ select { border-radius: 6px; background: var(--search-bg); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.78rem; outline: none; transition: border-color 200ms ease; @@ -703,7 +721,7 @@ select { background: var(--bg-secondary); color: var(--text-secondary); cursor: pointer; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.65rem; font-weight: 600; transition: all 150ms ease; @@ -744,13 +762,15 @@ select { 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-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; cursor: pointer; - transition: color 150ms ease, border-color 150ms ease; + transition: + color 150ms ease, + border-color 150ms ease; margin-bottom: -1px; white-space: nowrap; } @@ -801,12 +821,14 @@ select { align-items: center; gap: 6px; padding: 5px 16px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.82rem; color: var(--text-secondary); cursor: pointer; border-radius: 0; - transition: background 120ms ease, color 120ms ease; + transition: + background 120ms ease, + color 120ms ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -827,13 +849,19 @@ select { .tree-item.path-selected { background: color-mix(in srgb, var(--accent) 20%, transparent); color: var(--accent); - box-shadow: inset 0 0 0 2px var(--accent), inset 2px 0 0 var(--accent); + box-shadow: + inset 0 0 0 2px var(--accent), + inset 2px 0 0 var(--accent); border-radius: 6px; animation: pathPulse 1.5s ease-out; } @keyframes pathPulse { - 0% { background: color-mix(in srgb, var(--accent) 40%, transparent); } - 100% { background: color-mix(in srgb, var(--accent) 20%, transparent); } + 0% { + background: color-mix(in srgb, var(--accent) 40%, transparent); + } + 100% { + background: color-mix(in srgb, var(--accent) 20%, transparent); + } } .tree-item.filtered-out { display: none; @@ -933,7 +961,7 @@ select { background: var(--tag-bg); color: var(--tag-text); border-radius: 4px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; cursor: pointer; transition: opacity 150ms ease; line-height: 1.4; @@ -951,7 +979,9 @@ select { cursor: ew-resize; background: transparent; flex-shrink: 0; - transition: background 150ms ease, opacity 300ms ease; + transition: + background 150ms ease, + opacity 300ms ease; z-index: 10; } .sidebar-resize-handle:hover, @@ -970,7 +1000,9 @@ select { flex: 1; overflow-y: auto; padding: clamp(16px, 3vw, 40px) clamp(16px, 4vw, 40px) 60px; - transition: background 200ms ease, margin 300ms ease; + transition: + background 200ms ease, + margin 300ms ease; min-width: 0; } @@ -993,7 +1025,9 @@ select { cursor: ew-resize; background: transparent; flex-shrink: 0; - transition: background 150ms ease, opacity 300ms ease; + transition: + background 150ms ease, + opacity 300ms ease; z-index: 10; } .right-sidebar-resize-handle:hover, @@ -1017,7 +1051,10 @@ select { display: flex; flex-direction: column; overflow: hidden; - transition: background 200ms ease, transform 300ms ease, width 300ms ease; + transition: + background 200ms ease, + transform 300ms ease, + width 300ms ease; flex-shrink: 0; } @@ -1040,7 +1077,7 @@ select { } .right-sidebar-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; @@ -1108,7 +1145,7 @@ select { align-items: center; padding: 8px 12px; border-radius: 6px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.82rem; color: var(--text-secondary); cursor: pointer; @@ -1131,7 +1168,7 @@ select { } .outline-item.active::before { - content: ''; + content: ""; position: absolute; left: 0; top: 0; @@ -1202,7 +1239,7 @@ select { } .reading-progress-text { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; font-weight: 600; color: var(--text-muted); @@ -1221,7 +1258,7 @@ select { gap: 12px; } .welcome h2 { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 1.6rem; color: var(--text-secondary); } @@ -1232,7 +1269,7 @@ select { /* Breadcrumb */ .breadcrumb { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.78rem; color: var(--text-muted); margin-bottom: 12px; @@ -1262,7 +1299,7 @@ select { margin-bottom: 20px; } .file-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 1.6rem; font-weight: 700; margin-bottom: 8px; @@ -1279,12 +1316,14 @@ select { background: var(--tag-bg); color: var(--tag-text); border-radius: 4px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; margin-right: 6px; margin-bottom: 4px; cursor: pointer; - transition: opacity 150ms ease, transform 100ms ease; + transition: + opacity 150ms ease, + transform 100ms ease; } .file-tag:hover { opacity: 0.8; @@ -1298,7 +1337,7 @@ select { align-items: center; } .btn-action { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; padding: 4px 10px; border: 1px solid var(--border); @@ -1306,7 +1345,9 @@ select { background: transparent; color: var(--text-secondary); cursor: pointer; - transition: color 150ms ease, border-color 150ms ease; + transition: + color 150ms ease, + border-color 150ms ease; display: inline-flex; align-items: center; gap: 4px; @@ -1330,7 +1371,7 @@ select { /* Header toggle */ .fm-header { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.78rem; color: var(--text-muted); cursor: pointer; @@ -1538,7 +1579,7 @@ select { border-radius: 8px; padding: 16px; margin-top: 12px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.82rem; color: var(--text-secondary); white-space: pre-wrap; @@ -1550,22 +1591,35 @@ select { } /* --- Markdown Rendered Content --- */ -.md-content h1, .md-content h2, .md-content h3, -.md-content h4, .md-content h5, .md-content h6 { - font-family: 'JetBrains Mono', monospace; +.md-content h1, +.md-content h2, +.md-content h3, +.md-content h4, +.md-content h5, +.md-content h6 { + font-family: "JetBrains Mono", monospace; font-weight: 700; margin: 1.4em 0 0.5em; color: var(--text-primary); } -.md-content h1 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 6px; } -.md-content h2 { font-size: 1.25rem; } -.md-content h3 { font-size: 1.1rem; } +.md-content h1 { + font-size: 1.5rem; + border-bottom: 1px solid var(--border); + padding-bottom: 6px; +} +.md-content h2 { + font-size: 1.25rem; +} +.md-content h3 { + font-size: 1.1rem; +} .md-content p { margin: 0.6em 0; } -.md-content ul, .md-content ol { +.md-content ul, +.md-content ol { padding-left: 1.6em; margin: 0.5em 0; } @@ -1584,7 +1638,7 @@ select { } .md-content code { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; background: var(--code-bg); padding: 2px 6px; border-radius: 4px; @@ -1614,14 +1668,15 @@ select { margin: 0.8em 0; font-size: 0.92rem; } -.md-content th, .md-content td { +.md-content th, +.md-content td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; } .md-content th { background: var(--bg-secondary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-weight: 600; font-size: 0.85rem; } @@ -1669,7 +1724,7 @@ select { color: var(--danger); border: 1px dashed var(--danger); border-radius: 4px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.85rem; cursor: help; } @@ -1698,7 +1753,7 @@ select { padding: 0; } .search-results-header { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 16px; @@ -1724,7 +1779,7 @@ select { background: var(--tag-bg); color: var(--tag-text); border-radius: 999px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; line-height: 1.2; } @@ -1746,21 +1801,23 @@ select { border-radius: 8px; margin-bottom: 10px; cursor: pointer; - transition: background 120ms ease, border-color 120ms ease; + transition: + background 120ms ease, + border-color 120ms ease; } .search-result-item:hover { background: var(--bg-hover); border-color: var(--accent); } .search-result-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.92rem; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } .search-result-vault { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.72rem; color: var(--accent-green); margin-bottom: 4px; @@ -1814,7 +1871,9 @@ select { animation: spin 0.8s linear infinite; } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } /* --- Search progress bar --- */ @@ -1840,9 +1899,18 @@ select { animation: progress-indeterminate 1.5s ease-in-out infinite; } @keyframes progress-indeterminate { - 0% { width: 0%; margin-left: 0%; } - 50% { width: 40%; margin-left: 30%; } - 100% { width: 0%; margin-left: 100%; } + 0% { + width: 0%; + margin-left: 0%; + } + 50% { + width: 40%; + margin-left: 30%; + } + 100% { + width: 0%; + margin-left: 100%; + } } .search-time-badge { font-size: 0.7rem; @@ -1880,7 +1948,7 @@ select { display: flex; flex-direction: column; overflow: hidden; - box-shadow: 0 8px 32px rgba(0,0,0,0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); } .editor-header { display: flex; @@ -1895,7 +1963,7 @@ select { z-index: 10; } .editor-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.9rem; color: var(--text-primary); font-weight: 600; @@ -1963,7 +2031,7 @@ select { flex-shrink: 0; } .help-nav-title { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; @@ -2034,7 +2102,7 @@ select { .help-content h1, .help-content h2, .help-content h3 { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; color: var(--text-primary); } .help-content h1 { @@ -2066,7 +2134,7 @@ select { line-height: 1.6; } .help-content code { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px; @@ -2098,7 +2166,7 @@ select { border-radius: 999px; background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.72rem; margin-bottom: 12px; } @@ -2134,7 +2202,7 @@ select { overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border)); background: color-mix(in srgb, var(--bg-primary) 88%, black 12%); - box-shadow: 0 18px 44px rgba(0,0,0,0.28); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28); } .help-mini-window-bar { @@ -2285,7 +2353,7 @@ select { justify-content: center; background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-weight: 700; } @@ -2323,7 +2391,7 @@ select { font-size: 0.9rem; } .cm-scroller { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; overflow-y: auto !important; overflow-x: auto !important; height: auto; @@ -2340,7 +2408,7 @@ select { padding: 16px; background: var(--code-bg); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.9rem; line-height: 1.55; box-sizing: border-box; @@ -2456,7 +2524,7 @@ body.resizing-v { .hamburger-btn { display: flex; } - + .sidebar-toggle-btn { display: none; } @@ -2528,7 +2596,7 @@ body.resizing-v { width: calc(100vw - 20px); max-height: calc(100vh - 120px); border-radius: 12px; - box-shadow: 0 12px 40px rgba(0,0,0,0.4); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); } .search-dropdown__item { @@ -2556,28 +2624,28 @@ body.resizing-v { border-radius: 16px; } - .sidebar-quick-select { - padding: 0 16px 12px; - } + .sidebar-quick-select { + padding: 0 16px 12px; + } - .sidebar-quick-select select { - color-scheme: dark; - background-color: var(--bg-secondary); - color: var(--text-primary); - } + .sidebar-quick-select select { + color-scheme: dark; + background-color: var(--bg-secondary); + color: var(--text-primary); + } - [data-theme="dark"] .sidebar-quick-select select { - background-color: #1e1e1e; - color: #e0e0e0; - } + [data-theme="dark"] .sidebar-quick-select select { + background-color: #1e1e1e; + color: #e0e0e0; + } - .sidebar-panel-toggle { - padding: 8px 12px 10px; - } + .sidebar-panel-toggle { + padding: 8px 12px 10px; + } - .tag-cloud { - padding: 0 12px 12px; - } + .tag-cloud { + padding: 0 12px 12px; + } .sidebar { position: fixed; @@ -2589,12 +2657,14 @@ body.resizing-v { min-width: 280px !important; max-width: 85vw !important; transform: translateX(-100%); - transition: transform 250ms ease, background 200ms ease; + transition: + transform 250ms ease, + background 200ms ease; box-shadow: none; } .sidebar.mobile-open { transform: translateX(0); - box-shadow: 4px 0 20px rgba(0,0,0,0.3); + box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3); } /* Right Sidebar handling on mobile */ @@ -2608,7 +2678,9 @@ body.resizing-v { min-width: 280px !important; max-width: 85vw !important; transform: translateX(100%); - transition: transform 250ms ease, background 200ms ease; + transition: + transform 250ms ease, + background 200ms ease; box-shadow: none; border-left: 1px solid var(--border); } @@ -2620,7 +2692,7 @@ body.resizing-v { } .right-sidebar:not(.hidden) { transform: translateX(0) !important; - box-shadow: -4px 0 20px rgba(0,0,0,0.3) !important; + box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3) !important; display: flex !important; visibility: visible !important; } @@ -2650,19 +2722,19 @@ body.resizing-v { max-height: none; } - .help-content { - padding: 20px 16px 28px; - } + .help-content { + padding: 20px 16px 28px; + } - .help-hero { - grid-template-columns: 1fr; - padding: 18px; - } + .help-hero { + grid-template-columns: 1fr; + padding: 18px; + } - .help-grid, - .help-tip-grid { - grid-template-columns: 1fr; - } + .help-grid, + .help-tip-grid { + grid-template-columns: 1fr; + } .content-area { padding: 16px 12px 60px; @@ -2689,7 +2761,7 @@ body.resizing-v { } .config-section h2 { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 1.05rem; font-weight: 600; color: var(--text-primary); @@ -2719,7 +2791,7 @@ body.resizing-v { background: var(--tag-bg); color: var(--tag-text); border-radius: 6px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; border: 1px solid color-mix(in srgb, var(--tag-text) 30%, transparent); } @@ -2774,7 +2846,7 @@ body.resizing-v { border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; outline: none; transition: border-color 200ms ease; @@ -2790,7 +2862,7 @@ body.resizing-v { border-radius: 6px; background: var(--accent); color: #ffffff; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; font-weight: 600; cursor: pointer; @@ -2811,7 +2883,7 @@ body.resizing-v { } .config-regex-preview code { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; color: var(--accent); background: transparent; border: none; @@ -2863,21 +2935,28 @@ body.resizing-v { border-radius: 6px; background: var(--accent); color: #fff; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: opacity 150ms; } -.config-btn-save:hover { opacity: 0.9; } +.config-btn-save:hover { + opacity: 0.9; +} .config-btn-save.config-btn-highlight { background: var(--accent-green); border-color: var(--accent-green); animation: pulse 2s ease-in-out infinite; } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.8; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.8; + } } .config-btn-secondary { padding: 8px 16px; @@ -2885,12 +2964,14 @@ body.resizing-v { border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.8rem; cursor: pointer; transition: background 150ms; } -.config-btn-secondary:hover { background: var(--bg-hover); } +.config-btn-secondary:hover { + background: var(--bg-hover); +} /* --- Config diagnostics panel --- */ .config-diagnostics { @@ -2898,7 +2979,7 @@ body.resizing-v { border: 1px solid var(--border); border-radius: 6px; padding: 14px 16px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.78rem; line-height: 1.7; color: var(--text-secondary); @@ -2910,8 +2991,13 @@ body.resizing-v { display: flex; justify-content: space-between; } -.config-diag-row .diag-label { color: var(--text-secondary); } -.config-diag-row .diag-value { color: var(--text-primary); font-weight: 500; } +.config-diag-row .diag-label { + color: var(--text-secondary); +} +.config-diag-row .diag-value { + color: var(--text-primary); + font-weight: 500; +} .config-diag-section { margin-bottom: 8px; padding-bottom: 8px; @@ -2936,7 +3022,9 @@ body.resizing-v { grid-template-columns: 1fr; gap: 4px; } - .config-input--num { width: 100%; } + .config-input--num { + width: 100%; + } } /* --- Toast notifications --- */ @@ -2955,15 +3043,17 @@ body.resizing-v { .toast { padding: 10px 20px; border-radius: 8px; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.82rem; color: var(--text-primary); background: var(--bg-secondary); border: 1px solid var(--border); - box-shadow: 0 8px 24px rgba(0,0,0,0.3); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); opacity: 0; transform: translateY(12px); - transition: opacity 250ms ease, transform 250ms ease; + transition: + opacity 250ms ease, + transform 250ms ease; pointer-events: auto; max-width: 420px; text-align: center; @@ -3003,7 +3093,7 @@ body.resizing-v { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 8px 24px rgba(0,0,0,0.25); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); z-index: 200; max-height: 380px; overflow-y: auto; @@ -3048,7 +3138,7 @@ body.resizing-v { } .search-dropdown__clear-btn:hover { color: var(--danger); - background: var(--danger-bg, rgba(255,0,0,0.08)); + background: var(--danger-bg, rgba(255, 0, 0, 0.08)); } .search-dropdown__list { list-style: none; @@ -3138,7 +3228,7 @@ body.resizing-v { gap: 4px; padding: 3px 8px; font-size: 0.75rem; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; border-radius: 12px; background: var(--accent); color: var(--bg-primary); @@ -3234,7 +3324,9 @@ body.resizing-v { color: var(--text-secondary); background: var(--bg-secondary); cursor: pointer; - transition: border-color 120ms, background 120ms; + transition: + border-color 120ms, + background 120ms; } .search-facets__item:hover { border-color: var(--accent); @@ -3263,7 +3355,9 @@ body.resizing-v { background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; - transition: border-color 120ms, background 120ms; + transition: + border-color 120ms, + background 120ms; } .search-pagination__btn:hover:not(:disabled) { border-color: var(--accent); @@ -3329,31 +3423,38 @@ body.resizing-v { border-radius: 50%; margin-left: 8px; cursor: pointer; - transition: background 300ms, box-shadow 300ms; + transition: + background 300ms, + box-shadow 300ms; flex-shrink: 0; vertical-align: middle; } .sync-badge--disconnected { background: #ef4444; - box-shadow: 0 0 0 2px rgba(239,68,68,0.25); + box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.25); } .sync-badge--connecting { background: #f59e0b; - box-shadow: 0 0 0 2px rgba(245,158,11,0.25); + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25); animation: sync-pulse 1.5s ease-in-out infinite; } .sync-badge--connected { background: #22c55e; - box-shadow: 0 0 0 2px rgba(34,197,94,0.2); + box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2); } .sync-badge--syncing { background: #3b82f6; - box-shadow: 0 0 0 2px rgba(59,130,246,0.3); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); animation: sync-pulse 0.8s ease-in-out infinite; } @keyframes sync-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } } /* --------------------------------------------------------------------------- @@ -3368,7 +3469,7 @@ body.resizing-v { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; - box-shadow: 0 8px 32px rgba(0,0,0,0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); z-index: 9999; overflow: hidden; font-size: 0.82rem; @@ -3391,19 +3492,19 @@ body.resizing-v { font-weight: 500; } .sync-panel__state--connected { - background: rgba(34,197,94,0.15); + background: rgba(34, 197, 94, 0.15); color: #22c55e; } .sync-panel__state--disconnected { - background: rgba(239,68,68,0.15); + background: rgba(239, 68, 68, 0.15); color: #ef4444; } .sync-panel__state--connecting { - background: rgba(245,158,11,0.15); + background: rgba(245, 158, 11, 0.15); color: #f59e0b; } .sync-panel__state--syncing { - background: rgba(59,130,246,0.15); + background: rgba(59, 130, 246, 0.15); color: #3b82f6; } .sync-panel__empty { @@ -3480,7 +3581,9 @@ body.resizing-v { background: var(--bg-hover); border: 1px solid var(--border); border-radius: 22px; - transition: background 200ms, border-color 200ms; + transition: + background 200ms, + border-color 200ms; } .config-toggle-slider::before { content: ""; @@ -3491,10 +3594,12 @@ body.resizing-v { bottom: 2px; background: var(--text-muted); border-radius: 50%; - transition: transform 200ms, background 200ms; + transition: + transform 200ms, + background 200ms; } .config-toggle input:checked + .config-toggle-slider { - background: rgba(34,197,94,0.2); + background: rgba(34, 197, 94, 0.2); border-color: #22c55e; } .config-toggle input:checked + .config-toggle-slider::before { @@ -3649,7 +3754,9 @@ body.resizing-v { /* --------------------------------------------------------------------------- Utility — hidden class --------------------------------------------------------------------------- */ -.hidden { display: none !important; } +.hidden { + display: none !important; +} /* --------------------------------------------------------------------------- Login Screen @@ -3668,15 +3775,21 @@ body.resizing-v { max-width: 90vw; background: rgba(30, 30, 46, 0.85); backdrop-filter: blur(20px); - border: 1px solid rgba(255,255,255,0.08); + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; padding: 40px 36px; - box-shadow: 0 20px 60px rgba(0,0,0,0.5); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: loginFadeIn 0.4s ease-out; } @keyframes loginFadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.97); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { + opacity: 0; + transform: translateY(20px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } } .login-header { text-align: center; @@ -3703,7 +3816,7 @@ body.resizing-v { } .login-subtitle { font-size: 0.88rem; - color: rgba(255,255,255,0.45); + color: rgba(255, 255, 255, 0.45); margin: 0; } .login-form .form-group { @@ -3713,29 +3826,31 @@ body.resizing-v { display: block; font-size: 0.82rem; font-weight: 500; - color: rgba(255,255,255,0.6); + color: rgba(255, 255, 255, 0.6); margin-bottom: 6px; } .login-form input[type="text"], .login-form input[type="password"] { width: 100%; padding: 12px 14px; - border: 1px solid rgba(255,255,255,0.1); + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; - background: rgba(255,255,255,0.06); + background: rgba(255, 255, 255, 0.06); color: #e2e8f0; font-size: 0.92rem; font-family: inherit; outline: none; - transition: border-color 200ms, box-shadow 200ms; + transition: + border-color 200ms, + box-shadow 200ms; box-sizing: border-box; } .login-form input:focus { border-color: #818cf8; - box-shadow: 0 0 0 3px rgba(129,140,248,0.2); + box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2); } .login-form input::placeholder { - color: rgba(255,255,255,0.25); + color: rgba(255, 255, 255, 0.25); } .password-input-wrapper { position: relative; @@ -3753,7 +3868,9 @@ body.resizing-v { transition: opacity 150ms; padding: 4px; } -.password-toggle:hover { opacity: 0.9; } +.password-toggle:hover { + opacity: 0.9; +} .form-group--inline { display: flex; align-items: center; @@ -3764,7 +3881,7 @@ body.resizing-v { gap: 8px; cursor: pointer; font-size: 0.82rem; - color: rgba(255,255,255,0.5); + color: rgba(255, 255, 255, 0.5); } .checkbox-label input[type="checkbox"] { accent-color: #818cf8; @@ -3774,8 +3891,8 @@ body.resizing-v { .login-error { padding: 10px 14px; border-radius: 8px; - background: rgba(239,68,68,0.12); - border: 1px solid rgba(239,68,68,0.3); + background: rgba(239, 68, 68, 0.12); + border: 1px solid rgba(239, 68, 68, 0.3); color: #fca5a5; font-size: 0.82rem; margin-bottom: 16px; @@ -3790,7 +3907,9 @@ body.resizing-v { font-size: 0.95rem; font-weight: 600; cursor: pointer; - transition: opacity 200ms, transform 100ms; + transition: + opacity 200ms, + transform 100ms; display: flex; align-items: center; justify-content: center; @@ -3807,29 +3926,39 @@ body.resizing-v { opacity: 0.6; cursor: not-allowed; } -.btn-spinner { font-size: 1rem; } +.btn-spinner { + font-size: 1rem; +} /* Light theme login */ [data-theme="light"] .login-screen { background: linear-gradient(135deg, #dbeafe, #eff6ff, #bfdbfe); } [data-theme="light"] .login-card { - background: rgba(255,255,255,0.9); - border: 1px solid rgba(0,0,0,0.08); - box-shadow: 0 20px 60px rgba(0,0,0,0.12); + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12); +} +[data-theme="light"] .login-subtitle { + color: rgba(0, 0, 0, 0.45); +} +[data-theme="light"] .login-form .form-group label { + color: rgba(0, 0, 0, 0.6); } -[data-theme="light"] .login-subtitle { color: rgba(0,0,0,0.45); } -[data-theme="light"] .login-form .form-group label { color: rgba(0,0,0,0.6); } [data-theme="light"] .login-form input[type="text"], [data-theme="light"] .login-form input[type="password"] { - background: rgba(0,0,0,0.04); - border-color: rgba(0,0,0,0.12); + background: rgba(0, 0, 0, 0.04); + border-color: rgba(0, 0, 0, 0.12); color: #1e293b; } -[data-theme="light"] .login-form input::placeholder { color: rgba(0,0,0,0.3); } -[data-theme="light"] .checkbox-label { color: rgba(0,0,0,0.5); } +[data-theme="light"] .login-form input::placeholder { + color: rgba(0, 0, 0, 0.3); +} +[data-theme="light"] .checkbox-label { + color: rgba(0, 0, 0, 0.5); +} [data-theme="light"] .login-error { - background: rgba(239,68,68,0.08); + background: rgba(239, 68, 68, 0.08); color: #dc2626; } @@ -3861,14 +3990,18 @@ body.resizing-v { cursor: pointer; padding: 4px; border-radius: 4px; - transition: color 150ms, background 150ms; + transition: + color 150ms, + background 150ms; } .user-menu-link:hover, .btn-logout:hover { color: var(--text-primary); background: var(--bg-hover); } -.btn-logout:hover { color: var(--danger); } +.btn-logout:hover { + color: var(--danger); +} /* --------------------------------------------------------------------------- Admin Panel @@ -3902,7 +4035,9 @@ body.resizing-v { position: sticky; top: 0; } -.admin-table td { color: var(--text-primary); } +.admin-table td { + color: var(--text-primary); +} .admin-role-badge { display: inline-block; padding: 2px 10px; @@ -3913,11 +4048,11 @@ body.resizing-v { letter-spacing: 0.03em; } .admin-role-admin { - background: rgba(59,130,246,0.15); + background: rgba(59, 130, 246, 0.15); color: #3b82f6; } .admin-role-user { - background: rgba(34,197,94,0.12); + background: rgba(34, 197, 94, 0.12); color: #22c55e; } .admin-vaults-text { @@ -3936,14 +4071,18 @@ body.resizing-v { font-size: 0.9rem; transition: background 150ms; } -.admin-action-btn:hover { background: var(--bg-hover); } -.admin-action-btn.danger:hover { background: rgba(239,68,68,0.12); } +.admin-action-btn:hover { + background: var(--bg-hover); +} +.admin-action-btn.danger:hover { + background: rgba(239, 68, 68, 0.12); +} /* Admin form overlay */ .admin-form-overlay { position: absolute; inset: 0; - background: rgba(0,0,0,0.6); + background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; @@ -3959,7 +4098,7 @@ body.resizing-v { max-width: 90vw; max-height: 80vh; overflow-y: auto; - box-shadow: 0 12px 40px rgba(0,0,0,0.3); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); } .admin-form-card h3 { margin: 0 0 20px; @@ -4037,7 +4176,7 @@ body.popup-mode .content-area { height: calc(100vh - 30px); max-width: 1000px; overflow-y: auto; - box-shadow: 0 8px 30px rgba(0,0,0,0.15); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); box-sizing: border-box; } @@ -4046,7 +4185,12 @@ body.popup-mode .content-area { --------------------------------------------------------------------------- */ /* Liste récente */ -.recent-list { display: flex; flex-direction: column; gap: 2px; padding: 4px 0; } +.recent-list { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0; +} /* Item */ .recent-item { @@ -4054,57 +4198,111 @@ body.popup-mode .content-area { border-radius: 6px; cursor: pointer; border-left: 3px solid transparent; - transition: background 0.15s, border-color 0.15s; + transition: + background 0.15s, + border-color 0.15s; } .recent-item:hover { background: var(--bg-secondary); border-left-color: var(--accent); } -.recent-item.active { border-left-color: var(--accent); background: var(--bg-secondary); } +.recent-item.active { + border-left-color: var(--accent); + background: var(--bg-secondary); +} /* Header: temps + badge vault */ .recent-item-header { - display: flex; justify-content: space-between; align-items: center; + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 3px; } -.recent-time { font-size: 11px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; } +.recent-time { + font-size: 11px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} .recent-vault-badge { - font-size: 10px; font-weight: 600; padding: 1px 6px; - border-radius: 10px; background: var(--accent); color: #fff; opacity: 0.85; + font-size: 10px; + font-weight: 600; + padding: 1px 6px; + border-radius: 10px; + background: var(--accent); + color: #fff; + opacity: 0.85; } /* Titre */ -.recent-item-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 2px; } +.recent-item-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 2px; +} /* Breadcrumb path */ -.recent-item-path { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; font-family: monospace; } +.recent-item-path { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 4px; + font-family: monospace; +} /* Preview */ .recent-item-preview { - font-size: 12px; color: var(--text-muted); - display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; - overflow: hidden; line-height: 1.4; margin-bottom: 5px; + font-size: 12px; + color: var(--text-muted); + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; + margin-bottom: 5px; } /* Tags */ -.recent-item-tags { display: flex; flex-wrap: wrap; gap: 4px; } +.recent-item-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} .recent-item-tags .tag-pill { - font-size: 10px; padding: 1px 6px; border-radius: 10px; - background: var(--bg-secondary); color: var(--accent); border: 1px solid var(--border); + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + background: var(--bg-secondary); + color: var(--accent); + border: 1px solid var(--border); } /* Filtre vault */ -.recent-filter-bar { padding: 8px 12px 4px; } +.recent-filter-bar { + padding: 8px 12px 4px; +} .recent-filter-bar select { - width: 100%; padding: 4px 8px; font-size: 12px; - background: var(--bg-secondary); color: var(--text-primary); - border: 1px solid var(--border); border-radius: 4px; cursor: pointer; + width: 100%; + padding: 4px 8px; + font-size: 12px; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border); + border-radius: 4px; + cursor: pointer; } /* État vide */ .recent-empty { - display: flex; flex-direction: column; align-items: center; - padding: 32px 16px; color: var(--text-muted); gap: 8px; font-size: 13px; + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 16px; + color: var(--text-muted); + gap: 8px; + font-size: 13px; } /* --------------------------------------------------------------------------- @@ -4132,7 +4330,7 @@ body.popup-mode .content-area { border: 1px solid var(--border); border-radius: 12px; padding: 12px 16px; - box-shadow: 0 8px 32px rgba(0,0,0,0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); z-index: 1000; animation: slideUp 200ms ease; max-width: calc(100vw - 40px); @@ -4165,7 +4363,7 @@ body.popup-mode .content-area { border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.85rem; outline: none; transition: border-color 200ms ease; @@ -4177,7 +4375,7 @@ body.popup-mode .content-area { /* Compteur d'occurrences */ .find-counter { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.75rem; color: var(--text-secondary); white-space: nowrap; @@ -4228,7 +4426,7 @@ body.popup-mode .content-area { } .find-option-btn { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; font-size: 0.7rem; font-weight: 600; } @@ -4264,7 +4462,7 @@ body.popup-mode .content-area { border-radius: 6px; color: var(--danger); font-size: 0.75rem; - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; } .find-error[hidden] { @@ -4433,14 +4631,368 @@ body.popup-mode .content-area { right: 10px; left: 10px; } - + .pwa-update-content { padding: 12px 16px; font-size: 0.85rem; } - + .pwa-update-btn { padding: 6px 12px; font-size: 0.8rem; } } + +/* --------------------------------------------------------------------------- + Dashboard Home - Recent Files Widget + --------------------------------------------------------------------------- */ + +.dashboard-home { + padding: 24px; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.dashboard-header { + margin-bottom: 20px; +} + +.dashboard-title-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.dashboard-icon { + width: 28px; + height: 28px; + color: var(--accent); +} + +.dashboard-title-row h2 { + font-family: "JetBrains Mono", monospace; + font-size: 1.4rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.dashboard-filter-row { + display: flex; + align-items: center; + gap: 12px; +} + +.dashboard-select { + padding: 6px 12px; + font-size: 13px; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + min-width: 160px; +} + +.dashboard-select:focus { + outline: none; + border-color: var(--accent); +} + +.dashboard-count { + font-size: 12px; + color: var(--text-muted); + padding: 4px 10px; + background: var(--bg-secondary); + border-radius: 12px; +} + +/* Grid */ +.dashboard-recent-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + flex: 1; +} + +@media (max-width: 1200px) { + .dashboard-recent-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .dashboard-home { + padding: 16px; + } + + .dashboard-recent-grid { + grid-template-columns: 1fr; + } + + .dashboard-title-row h2 { + font-size: 1.2rem; + } +} + +/* Card */ +.dashboard-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 12px; + padding: 16px; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + animation: fadeSlideIn 0.3s ease forwards; + opacity: 0; +} + +.dashboard-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + border-color: var(--accent); +} + +.dashboard-card:active { + transform: translateY(0); +} + +@keyframes fadeSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Staggered animation for cards */ +.dashboard-card:nth-child(1) { + animation-delay: 0ms; +} +.dashboard-card:nth-child(2) { + animation-delay: 50ms; +} +.dashboard-card:nth-child(3) { + animation-delay: 100ms; +} +.dashboard-card:nth-child(4) { + animation-delay: 150ms; +} +.dashboard-card:nth-child(5) { + animation-delay: 200ms; +} +.dashboard-card:nth-child(6) { + animation-delay: 250ms; +} +.dashboard-card:nth-child(7) { + animation-delay: 300ms; +} +.dashboard-card:nth-child(8) { + animation-delay: 350ms; +} +.dashboard-card:nth-child(9) { + animation-delay: 400ms; +} + +/* Card header */ +.dashboard-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.dashboard-card-icon { + width: 36px; + height: 36px; + padding: 8px; + background: var(--accent); + border-radius: 8px; + color: white; + flex-shrink: 0; +} + +.dashboard-card-icon svg { + width: 20px; + height: 20px; +} + +.dashboard-vault-badge { + font-size: 10px; + font-weight: 600; + padding: 3px 8px; + border-radius: 10px; + background: var(--accent); + color: white; + opacity: 0.9; +} + +/* Card title */ +.dashboard-card-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* Card path */ +.dashboard-card-path { + font-size: 11px; + color: var(--text-muted); + font-family: "JetBrains Mono", monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Card preview */ +.dashboard-card-preview { + font-size: 12px; + color: var(--text-muted); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin: 0; +} + +/* Card footer */ +.dashboard-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: auto; + padding-top: 8px; + border-top: 1px solid var(--border); +} + +.dashboard-card-time { + font-size: 11px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} + +.dashboard-card-time svg { + width: 12px; + height: 12px; +} + +/* Card tags */ +.dashboard-card-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.dashboard-card-tags .tag-pill { + font-size: 10px; + padding: 2px 6px; + border-radius: 8px; + background: var(--bg-tertiary); + color: var(--accent); + border: 1px solid var(--border); +} + +/* Empty state */ +.dashboard-recent-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + color: var(--text-muted); + gap: 12px; + text-align: center; + flex: 1; +} + +.dashboard-recent-empty svg { + width: 48px; + height: 48px; + opacity: 0.4; +} + +.dashboard-recent-empty span { + font-size: 16px; + font-weight: 500; +} + +.dashboard-recent-empty p { + font-size: 13px; + margin: 0; + opacity: 0.7; +} + +/* Loading skeletons */ +.dashboard-loading { + display: none; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.dashboard-loading.active { + display: grid; +} + +@media (max-width: 1200px) { + .dashboard-loading { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .dashboard-loading { + grid-template-columns: 1fr; + } +} + +.skeleton-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 12px; + padding: 16px; + height: 160px; + position: relative; + overflow: hidden; +} + +.skeleton-card::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent); + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} diff --git a/plans/SPEC_Dashboard_RecentFiles.md b/plans/SPEC_Dashboard_RecentFiles.md new file mode 100644 index 0000000..26c4e14 --- /dev/null +++ b/plans/SPEC_Dashboard_RecentFiles.md @@ -0,0 +1,161 @@ +# SPEC: Widget "Derniers fichiers ouverts" sur la page principale + +## 1. Objectif + +Remplacer le message d'accueil actuel (`#welcome`) par un widget dashboard professionnel affichant les **XX derniers fichiers ouverts**. Le nombre de fichiers affichés sera configurable via les paramètres existants (`cfg-recent-limit`). + +## 2. Architecture du composant + +```mermaid +graph TD + A["#content-area"] --> B["#dashboard-home"] + B --> C["En-tête: Logo + Titre"] + B --> D["Sélecteur vault filter"] + B --> E["#dashboard-recent-grid"] + B --> F["#dashboard-recent-empty"] + + E --> G["Carte 1: fichier récent"] + E --> H["Carte 2: fichier récent"] + E --> I["..."] + + G --> G1["Icône fichier"] + G --> G2["Titre"] + G --> G3["Chemin breadcrumb"] + G --> G4["Tags"] + G --> G5["Horodatage"] +``` + +## 3. Design UI/UX + +### 3.1 Structure HTML (dans `#content-area`) + +```html +
+
+
+ +

Derniers fichiers ouverts

+
+
+ + +
+
+
+ +
+``` + +### 3.2 Layout Grid + +- **Desktop (>1024px)**: Grid 3 colonnes +- **Tablet (768-1024px)**: Grid 2 colonnes +- **Mobile (<768px)**: Grid 1 colonne + +### 3.3 Style des cartes (`.dashboard-card`) + +| Élément | Style | +|---------|-------| +| Container | `border-radius: 12px`, `padding: 16px`, fond `var(--bg-secondary)` | +| Hover | Légère élévation avec `box-shadow`, bordure accent | +| Icône | 32x32px, couleur accent | +| Titre | `font-weight: 600`, `font-size: 14px`, truncation avec ellipsis | +| Chemin | `font-size: 11px`, `color: var(--text-muted)` | +| Tags | Pills compacts, même style que sidebar | +| Horodatage | Badge discret avec icône clock | +| Badge vault | Position absolute en haut-droit | + +### 3.4 États + +| État | Description | +|------|-------------| +| **Chargement** | Skeleton cards animées (pulse) | +| **Données** | Grid de cartes | +| **Vide** | Icône + message centré | +| **Erreur** | Toast notification | + +## 4. Interactions + +| Action | Comportement | +|--------|--------------| +| **Clic sur carte** | Ouvre le fichier (`openFile(vault, path)`) | +| **Changement filter vault** | Recharge les données avec filtre | +| **Hover carte** | Élévation visuelle, curseur pointer | +| **Fermeture fichier** | Affiche le dashboard (si plus de fichier ouvert) | + +## 5. Intégration JavaScript + +### 5.1 Nouveau module `DashboardRecentWidget` + +```javascript +const DashboardRecentWidget = { + async load(vaultFilter) { /* Charge via /api/recent */ }, + render(files) { /* Génère le HTML des cartes */ }, + showEmpty() { /* Affiche état vide */ }, + showLoading() { /* Affiche skeletons */ }, + init() { /* Bind events, charge initial */ } +}; +``` + +### 5.2 Points d'intégration + +- **Au démarrage**: `DashboardRecentWidget.init()` dans `DOMContentLoaded` +- **Ouverture fichier**: Dashboard masqué automatiquement +- **Fermeture fichier**: Dashboard affiché si `activeEditor === null` +- **Filtre vault**: Recharge via `loadRecentFiles` de la sidebar (partage du cache) + +### 5.3 Partage avec sidebar "Recent" + +Le widget réutilisera `_recentFilesCache` et `renderRecentList` mais avec un **template de carte différent** pour le dashboard. + +## 6. Fichiers à modifier + +| Fichier | Modification | +|---------|--------------| +| `frontend/index.html` | Remplacer `#welcome` par `#dashboard-home` | +| `frontend/style.css` | Ajouter `.dashboard-home`, `.dashboard-card`, skeleton, animations | +| `frontend/app.js` | Ajouter `DashboardRecentWidget`, modifier `showWelcome()`, intégration hooks | + +## 7. Paramètres configurables + +| Paramètre | Source | Description | +|-----------|--------|-------------| +| `recent_files_limit` | Backend config | Nombre max de fichiers (5-100, défaut: 20) | + +## 8. Accessibilité + +- Rôles ARIA appropriés (`role="region"`, `aria-label`) +- Navigation clavier fonctionnelle +- Contraste des couleurs conforme WCAG 2.1 AA +- Support screen reader pour les états vides/chargement + +## 9. Responsive breakpoints + +```css +.dashboard-recent-grid { + display: grid; + gap: 16px; + /* Mobile first */ + grid-template-columns: 1fr; +} +@media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); +} +@media (min-width: 1024px) { + grid-template-columns: repeat(3, 1fr); +} +``` + +## 10. Animations + +| Événement | Animation | +|-----------|-----------| +| Entrée cartes | Fade-in + slide-up staggeré (50ms entre cartes) | +| Hover carte | Scale 1.02 + shadow | +| Skeleton loading | Pulse animation | +| Transition vault filter | Fade out/in des cartes |