/* ObsiGate — Vanilla JS SPA */ (function () { "use strict"; // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let currentVault = null; let currentPath = null; let searchTimeout = null; let showingSource = false; let cachedRawSource = null; let allVaults = []; let selectedContextVault = "all"; let selectedTags = []; let editorView = null; let editorVault = null; let editorPath = null; let fallbackEditorEl = null; const panelState = { vault: true, tag: true, }; // --------------------------------------------------------------------------- // File extension → Lucide icon mapping // --------------------------------------------------------------------------- const EXT_ICONS = { ".md": "file-text", ".txt": "file-text", ".log": "file-text", ".py": "file-code", ".js": "file-code", ".ts": "file-code", ".jsx": "file-code", ".tsx": "file-code", ".html": "file-code", ".css": "file-code", ".scss": "file-code", ".less": "file-code", ".json": "file-json", ".yaml": "file-cog", ".yml": "file-cog", ".toml": "file-cog", ".xml": "file-code", ".sh": "terminal", ".bash": "terminal", ".zsh": "terminal", ".bat": "terminal", ".cmd": "terminal", ".ps1": "terminal", ".java": "file-code", ".c": "file-code", ".cpp": "file-code", ".h": "file-code", ".hpp": "file-code", ".cs": "file-code", ".go": "file-code", ".rs": "file-code", ".rb": "file-code", ".php": "file-code", ".sql": "database", ".csv": "table", ".ini": "file-cog", ".cfg": "file-cog", ".conf": "file-cog", ".env": "file-cog", }; function getFileIcon(name) { const ext = "." + name.split(".").pop().toLowerCase(); return EXT_ICONS[ext] || "file"; } // --------------------------------------------------------------------------- // Safe CDN helpers // --------------------------------------------------------------------------- function safeCreateIcons() { if (typeof lucide !== "undefined" && lucide.createIcons) { 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 */ } } } // --------------------------------------------------------------------------- // Theme // --------------------------------------------------------------------------- function initTheme() { const saved = localStorage.getItem("obsigate-theme") || "dark"; applyTheme(saved); } function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("obsigate-theme", theme); // Update theme button icon and label const themeBtn = document.getElementById("theme-toggle"); const themeLabel = document.getElementById("theme-label"); if (themeBtn && themeLabel) { const icon = themeBtn.querySelector("i"); if (icon) { icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun"); } themeLabel.textContent = theme === "dark" ? "Sombre" : "Clair"; safeCreateIcons(); } // Swap highlight.js theme const darkSheet = document.getElementById("hljs-theme-dark"); const lightSheet = document.getElementById("hljs-theme-light"); if (darkSheet && lightSheet) { darkSheet.disabled = theme !== "dark"; lightSheet.disabled = theme !== "light"; } } function toggleTheme() { const current = document.documentElement.getAttribute("data-theme"); applyTheme(current === "dark" ? "light" : "dark"); } function initHeaderMenu() { const menuBtn = document.getElementById("header-menu-btn"); const menuDropdown = document.getElementById("header-menu-dropdown"); if (!menuBtn || !menuDropdown) return; menuBtn.addEventListener("click", (e) => { e.stopPropagation(); menuBtn.classList.toggle("active"); menuDropdown.classList.toggle("active"); }); // Close menu when clicking outside document.addEventListener("click", (e) => { if (!menuDropdown.contains(e.target) && e.target !== menuBtn) { menuBtn.classList.remove("active"); menuDropdown.classList.remove("active"); } }); // Prevent menu from closing when clicking inside menuDropdown.addEventListener("click", (e) => { e.stopPropagation(); }); } function closeHeaderMenu() { const menuBtn = document.getElementById("header-menu-btn"); const menuDropdown = document.getElementById("header-menu-dropdown"); if (!menuBtn || !menuDropdown) return; menuBtn.classList.remove("active"); menuDropdown.classList.remove("active"); } // --------------------------------------------------------------------------- // API helpers // --------------------------------------------------------------------------- async function api(path) { const res = await fetch(path); if (!res.ok) throw new Error(`API error: ${res.status}`); return res.json(); } // --------------------------------------------------------------------------- // Mobile sidebar // --------------------------------------------------------------------------- function initMobile() { const hamburger = document.getElementById("hamburger-btn"); const overlay = document.getElementById("sidebar-overlay"); const sidebar = document.getElementById("sidebar"); hamburger.addEventListener("click", () => { sidebar.classList.toggle("mobile-open"); overlay.classList.toggle("active"); }); overlay.addEventListener("click", () => { sidebar.classList.remove("mobile-open"); overlay.classList.remove("active"); }); } function closeMobileSidebar() { const sidebar = document.getElementById("sidebar"); const overlay = document.getElementById("sidebar-overlay"); if (sidebar) sidebar.classList.remove("mobile-open"); if (overlay) overlay.classList.remove("active"); } // --------------------------------------------------------------------------- // Vault context switching // --------------------------------------------------------------------------- function initVaultContext() { const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); if (!filter || !quickSelect) return; filter.addEventListener("change", async () => { await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" }); }); quickSelect.addEventListener("change", async () => { await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" }); }); } async function setSelectedVaultContext(vaultName, options) { selectedContextVault = vaultName; showingSource = false; cachedRawSource = null; syncVaultSelectors(); await refreshSidebarForContext(); await refreshTagsForContext(); showWelcome(); if (options && options.focusVault && vaultName !== "all") { await focusVaultInSidebar(vaultName); } } function syncVaultSelectors() { const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); if (filter) filter.value = selectedContextVault; if (quickSelect) quickSelect.value = selectedContextVault; } async function refreshSidebarForContext() { const container = document.getElementById("vault-tree"); container.innerHTML = ""; 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), icon("database", 16), document.createTextNode(` ${v.name} `), smallBadge(v.file_count), ]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); container.appendChild(vaultItem); const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` }); container.appendChild(childContainer); }); safeCreateIcons(); } async function focusVaultInSidebar(vaultName) { setPanelExpanded("vault", true); const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`); if (!vaultItem) return; document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused")); vaultItem.classList.add("focused"); const childContainer = document.getElementById(`vault-children-${vaultName}`); if (childContainer && childContainer.classList.contains("collapsed")) { await toggleVault(vaultItem, vaultName, true); } vaultItem.scrollIntoView({ block: "nearest" }); } async function refreshTagsForContext() { const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`; const data = await api(`/api/tags${vaultParam}`); renderTagCloud(data.tags); } // --------------------------------------------------------------------------- // Sidebar — Vault tree // --------------------------------------------------------------------------- async function loadVaults() { const vaults = await api("/api/vaults"); allVaults = vaults; const container = document.getElementById("vault-tree"); const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); container.innerHTML = ""; filter.innerHTML = ''; quickSelect.innerHTML = ''; vaults.forEach((v) => { // Sidebar tree entry const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [ icon("chevron-right", 14), icon("database", 16), document.createTextNode(` ${v.name} `), smallBadge(v.file_count), ]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); container.appendChild(vaultItem); const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` }); container.appendChild(childContainer); // Vault filter dropdown const opt = document.createElement("option"); opt.value = v.name; opt.textContent = v.name; filter.appendChild(opt); const quickOpt = document.createElement("option"); quickOpt.value = v.name; quickOpt.textContent = v.name; quickSelect.appendChild(quickOpt); }); syncVaultSelectors(); safeCreateIcons(); } async function toggleVault(itemEl, vaultName, forceExpand) { const childContainer = document.getElementById(`vault-children-${vaultName}`); if (!childContainer) return; const shouldExpand = forceExpand || childContainer.classList.contains("collapsed"); if (shouldExpand) { // Expand — load children if empty if (childContainer.children.length === 0) { await loadDirectory(vaultName, "", childContainer); } childContainer.classList.remove("collapsed"); // Swap chevron const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { childContainer.classList.add("collapsed"); const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } } async function loadDirectory(vaultName, dirPath, container) { const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`; const data = await api(url); container.innerHTML = ""; const fragment = document.createDocumentFragment(); data.items.forEach((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), ]); fragment.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); fragment.appendChild(subContainer); dirItem.addEventListener("click", async () => { if (subContainer.classList.contains("collapsed")) { if (subContainer.children.length === 0) { await loadDirectory(vaultName, item.path, subContainer); } subContainer.classList.remove("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { subContainer.classList.add("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } }); } 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}`), ]); fileItem.addEventListener("click", () => { openFile(vaultName, item.path); closeMobileSidebar(); }); fragment.appendChild(fileItem); } }); container.appendChild(fragment); safeCreateIcons(); } // --------------------------------------------------------------------------- // Sidebar filter // --------------------------------------------------------------------------- function initSidebarFilter() { const input = document.getElementById("sidebar-filter-input"); input.addEventListener("input", () => { const q = input.value.trim().toLowerCase(); filterSidebarTree(q); filterTagCloud(q); }); } function filterSidebarTree(query) { const tree = document.getElementById("vault-tree"); const items = tree.querySelectorAll(".tree-item"); if (!query) { items.forEach((item) => item.classList.remove("filtered-out")); tree.querySelectorAll(".tree-children").forEach((c) => c.classList.remove("filtered-out")); return; } // First pass: mark all as filtered out items.forEach((item) => item.classList.add("filtered-out")); tree.querySelectorAll(".tree-children").forEach((c) => c.classList.add("filtered-out")); // Second pass: show matching items and their ancestors items.forEach((item) => { const text = item.textContent.toLowerCase(); if (text.includes(query)) { item.classList.remove("filtered-out"); // Show all ancestor containers let parent = item.parentElement; while (parent && parent !== tree) { parent.classList.remove("filtered-out"); if (parent.classList.contains("tree-children")) { parent.classList.remove("collapsed"); } parent = parent.parentElement; } } }); } function filterTagCloud(query) { const tags = document.querySelectorAll("#tag-cloud .tag-item"); tags.forEach((tag) => { const text = tag.textContent.toLowerCase(); if (!query || text.includes(query)) { tag.classList.remove("filtered-out"); } else { tag.classList.add("filtered-out"); } }); } // --------------------------------------------------------------------------- // Tags // --------------------------------------------------------------------------- async function loadTags() { const data = await api("/api/tags"); renderTagCloud(data.tags); } function renderTagCloud(tags) { const cloud = document.getElementById("tag-cloud"); cloud.innerHTML = ""; const counts = Object.values(tags); if (counts.length === 0) return; const maxCount = Math.max(...counts); const minSize = 0.7; const maxSize = 1.25; 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}`), ]); tagEl.addEventListener("click", () => searchByTag(tag)); cloud.appendChild(tagEl); }); } function addTagFilter(tag) { if (!selectedTags.includes(tag)) { selectedTags.push(tag); performTagSearch(); } } function removeTagFilter(tag) { selectedTags = selectedTags.filter(t => t !== tag); if (selectedTags.length > 0) { performTagSearch(); } else { const input = document.getElementById("search-input"); if (input.value.trim()) { performSearch(input.value.trim(), document.getElementById("vault-filter").value, null); } else { showWelcome(); } } } function performTagSearch() { const input = document.getElementById("search-input"); const query = input.value.trim(); const vault = document.getElementById("vault-filter").value; performSearch(query, vault, selectedTags.length > 0 ? selectedTags.join(",") : null); } function buildSearchResultsHeader(data, query, tagFilter) { const header = el("div", { class: "search-results-header" }); const summaryText = el("span", { class: "search-results-summary-text" }); if (query && tagFilter) { summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`; } else if (query) { summaryText.textContent = `${data.count} résultat(s) pour "${query}"`; } else if (tagFilter) { summaryText.textContent = `${data.count} fichier(s) avec les tags`; } else { summaryText.textContent = `${data.count} résultat(s)`; } header.appendChild(summaryText); 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("×")]); removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeTagFilter(tag); }); const chip = el("span", { class: "search-results-active-tag" }, [ document.createTextNode(`#${tag}`), removeBtn, ]); activeTags.appendChild(chip); }); header.appendChild(activeTags); } return header; } function searchByTag(tag) { addTagFilter(tag); } // --------------------------------------------------------------------------- // File viewer // --------------------------------------------------------------------------- async function openFile(vaultName, filePath) { currentVault = vaultName; currentPath = filePath; showingSource = false; cachedRawSource = null; // Highlight active document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active")); const selector = `.tree-item[data-vault="${vaultName}"][data-path="${CSS.escape(filePath)}"]`; try { const active = document.querySelector(selector); if (active) active.classList.add("active"); } catch (e) { /* selector might fail on special chars */ } const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`; const data = await api(url); renderFile(data); } function renderFile(data) { const area = document.getElementById("content-area"); // Breadcrumb const parts = data.path.split("/"); const breadcrumbEls = []; breadcrumbEls.push(makeBreadcrumbSpan(data.vault, () => {})); 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, () => {})); } else { breadcrumbEls.push(el("span", {}, [document.createTextNode(part.replace(/\.md$/i, ""))])); } }); const breadcrumb = el("div", { class: "breadcrumb" }, breadcrumbEls); // Tags const tagsDiv = el("div", { class: "file-tags" }); (data.tags || []).forEach((tag) => { const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); t.addEventListener("click", () => searchByTag(tag)); tagsDiv.appendChild(t); }); // Action buttons const copyBtn = el("button", { class: "btn-action", title: "Copier le chemin" }, [ icon("copy", 14), document.createTextNode("Copier"), ]); copyBtn.addEventListener("click", () => { navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => { copyBtn.querySelector("span") || (copyBtn.lastChild.textContent = "Copié !"); copyBtn.lastChild.textContent = "Copié !"; setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500); }); }); 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"), ]); downloadBtn.addEventListener("click", () => { const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`; const a = document.createElement("a"); a.href = dlUrl; a.download = data.path.split("/").pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }); const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [ icon("edit", 14), document.createTextNode("Éditer"), ]); editBtn.addEventListener("click", () => { openEditor(data.vault, data.path); }); // Frontmatter let fmSection = null; if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { const fmToggle = el("div", { class: "frontmatter-toggle" }, [ document.createTextNode("▶ Frontmatter"), ]); const fmContent = el("div", { class: "frontmatter-content" }, [ document.createTextNode(JSON.stringify(data.frontmatter, null, 2)), ]); fmToggle.addEventListener("click", () => { fmContent.classList.toggle("open"); fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter"; }); fmSection = el("div", {}, [fmToggle, fmContent]); } // Content container (rendered HTML) const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" }); mdDiv.innerHTML = data.html; // Raw source container (hidden initially) const rawDiv = el("div", { class: "raw-source-view", id: "file-raw-content", style: "display:none" }); // Source button toggle logic sourceBtn.addEventListener("click", async () => { const rendered = document.getElementById("file-rendered-content"); const raw = document.getElementById("file-raw-content"); if (!rendered || !raw) return; showingSource = !showingSource; if (showingSource) { sourceBtn.classList.add("active"); if (!cachedRawSource) { const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`; const rawData = await api(rawUrl); cachedRawSource = rawData.raw; } raw.textContent = cachedRawSource; rendered.style.display = "none"; raw.style.display = "block"; } else { sourceBtn.classList.remove("active"); rendered.style.display = "block"; raw.style.display = "none"; } }); // 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]), ])); if (fmSection) area.appendChild(fmSection); area.appendChild(mdDiv); area.appendChild(rawDiv); // Highlight code blocks area.querySelectorAll("pre code").forEach((block) => { safeHighlight(block); }); // Wire up wikilinks area.querySelectorAll(".wikilink").forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const v = link.getAttribute("data-vault"); const p = link.getAttribute("data-path"); if (v && p) openFile(v, p); }); }); safeCreateIcons(); area.scrollTop = 0; } // --------------------------------------------------------------------------- // Collapsible panels and help modal // --------------------------------------------------------------------------- function initCollapsiblePanels() { bindPanelToggle("vault", "vault-panel-toggle", "vault-panel-content"); bindPanelToggle("tag", "tag-panel-toggle", "tag-panel-content"); setPanelExpanded("vault", true); setPanelExpanded("tag", true); } function bindPanelToggle(panelKey, toggleId, contentId) { const toggle = document.getElementById(toggleId); const content = document.getElementById(contentId); if (!toggle || !content) return; toggle.addEventListener("click", () => { setPanelExpanded(panelKey, !panelState[panelKey]); }); } function setPanelExpanded(panelKey, expanded) { panelState[panelKey] = expanded; const toggle = document.getElementById(`${panelKey}-panel-toggle`); const content = document.getElementById(`${panelKey}-panel-content`); if (!toggle || !content) return; toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); content.classList.toggle("collapsed", !expanded); const iconEl = toggle.querySelector("[data-lucide]"); if (iconEl) { iconEl.setAttribute("data-lucide", expanded ? "chevron-down" : "chevron-right"); } if (panelKey === "tag") { const tagSection = document.getElementById("tag-cloud-section"); const resizeHandle = document.getElementById("tag-resize-handle"); if (tagSection) tagSection.classList.toggle("collapsed-panel", !expanded); if (resizeHandle) resizeHandle.classList.toggle("hidden", !expanded); } safeCreateIcons(); } function initHelpModal() { const openBtn = document.getElementById("help-open-btn"); const closeBtn = document.getElementById("help-close"); const modal = document.getElementById("help-modal"); if (!openBtn || !closeBtn || !modal) return; openBtn.addEventListener("click", () => { modal.classList.add("active"); closeHeaderMenu(); safeCreateIcons(); }); closeBtn.addEventListener("click", closeHelpModal); modal.addEventListener("click", (e) => { if (e.target === modal) { closeHelpModal(); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("active")) { closeHelpModal(); } }); } function closeHelpModal() { const modal = document.getElementById("help-modal"); if (modal) modal.classList.remove("active"); } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- function initSearch() { const input = document.getElementById("search-input"); input.addEventListener("input", () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; if (q.length > 0 || tagFilter) { performSearch(q, vault, tagFilter); } else { showWelcome(); } }, 300); }); } async function performSearch(query, vaultFilter, tagFilter) { showLoading(); let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; const data = await api(url); renderSearchResults(data, query, tagFilter); } function renderSearchResults(data, query, tagFilter) { const area = document.getElementById("content-area"); area.innerHTML = ""; 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é."), ])); return; } const container = el("div", { class: "search-results" }); data.results.forEach((r) => { const item = el("div", { class: "search-result-item" }, [ el("div", { class: "search-result-title" }, [document.createTextNode(r.title)]), el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), el("div", { class: "search-result-snippet" }, [document.createTextNode(r.snippet || "")]), ]); if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); tagEl.addEventListener("click", (e) => { e.stopPropagation(); addTagFilter(tag); }); tagsDiv.appendChild(tagEl); }); item.appendChild(tagsDiv); } item.addEventListener("click", () => openFile(r.vault, r.path)); container.appendChild(item); }); area.appendChild(container); } // --------------------------------------------------------------------------- // Resizable sidebar (horizontal) // --------------------------------------------------------------------------- function initSidebarResize() { const handle = document.getElementById("sidebar-resize-handle"); const sidebar = document.getElementById("sidebar"); if (!handle || !sidebar) return; // Restore saved width const savedWidth = localStorage.getItem("obsigate-sidebar-width"); if (savedWidth) { sidebar.style.width = savedWidth + "px"; } let startX = 0; let startWidth = 0; function onMouseMove(e) { const newWidth = Math.min(500, Math.max(200, startWidth + (e.clientX - startX))); sidebar.style.width = newWidth + "px"; } function onMouseUp() { document.body.classList.remove("resizing"); handle.classList.remove("active"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); localStorage.setItem("obsigate-sidebar-width", parseInt(sidebar.style.width)); } handle.addEventListener("mousedown", (e) => { e.preventDefault(); startX = e.clientX; startWidth = sidebar.getBoundingClientRect().width; document.body.classList.add("resizing"); handle.classList.add("active"); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); } // --------------------------------------------------------------------------- // Resizable tag section (vertical) // --------------------------------------------------------------------------- function initTagResize() { const handle = document.getElementById("tag-resize-handle"); const tagSection = document.getElementById("tag-cloud-section"); if (!handle || !tagSection) return; // Restore saved height const savedHeight = localStorage.getItem("obsigate-tag-height"); if (savedHeight) { tagSection.style.height = savedHeight + "px"; } let startY = 0; let startHeight = 0; function onMouseMove(e) { // Dragging up increases height, dragging down decreases const newHeight = Math.min(400, Math.max(60, startHeight - (e.clientY - startY))); tagSection.style.height = newHeight + "px"; } function onMouseUp() { document.body.classList.remove("resizing-v"); handle.classList.remove("active"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); localStorage.setItem("obsigate-tag-height", parseInt(tagSection.style.height)); } handle.addEventListener("mousedown", (e) => { e.preventDefault(); startY = e.clientY; startHeight = tagSection.getBoundingClientRect().height; document.body.classList.add("resizing-v"); handle.classList.add("active"); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function el(tag, attrs, children) { const e = document.createElement(tag); if (attrs) { Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v)); } if (children) { children.forEach((c) => { if (c) e.appendChild(c); }); } return e; } function icon(name, size) { const i = document.createElement("i"); i.setAttribute("data-lucide", name); i.style.width = size + "px"; i.style.height = size + "px"; i.classList.add("icon"); return i; } function smallBadge(count) { const s = document.createElement("span"); s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px"; s.textContent = `(${count})`; return s; } function makeBreadcrumbSpan(text, onClick) { const s = document.createElement("span"); s.textContent = text; if (onClick) s.addEventListener("click", onClick); return s; } function showWelcome() { const area = document.getElementById("content-area"); area.innerHTML = `
Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.