diff --git a/backend/indexer.py b/backend/indexer.py index 5b2ca68..d1c0bf6 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -15,6 +15,18 @@ index: Dict[str, Dict[str, Any]] = {} # Vault config: {name: path} vault_config: Dict[str, str] = {} +# Supported text-based file extensions +SUPPORTED_EXTENSIONS = { + ".md", ".txt", ".log", ".py", ".js", ".ts", ".jsx", ".tsx", + ".sh", ".bash", ".zsh", ".fish", ".bat", ".cmd", ".ps1", + ".json", ".yaml", ".yml", ".toml", ".xml", ".csv", + ".cfg", ".ini", ".conf", ".env", + ".html", ".css", ".scss", ".less", + ".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".rb", + ".php", ".sql", ".r", ".m", ".swift", ".kt", + ".dockerfile", ".makefile", ".cmake", +} + def load_vault_config() -> Dict[str, str]: """Read VAULT_N_NAME / VAULT_N_PATH env vars and return {name: path}.""" @@ -60,18 +72,34 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: logger.warning(f"Vault path does not exist: {vault_path}") return {"files": [], "tags": {}, "path": vault_path} - for md_file in vault_root.rglob("*.md"): + for fpath in vault_root.rglob("*"): + if not fpath.is_file(): + continue + # Skip hidden files and files inside hidden directories + rel_parts = fpath.relative_to(vault_root).parts + if any(part.startswith(".") for part in rel_parts): + continue + ext = fpath.suffix.lower() + # Also match extensionless files named like Dockerfile, Makefile + basename_lower = fpath.name.lower() + if ext not in SUPPORTED_EXTENSIONS and basename_lower not in ("dockerfile", "makefile", "cmakelists.txt"): + continue try: - relative = md_file.relative_to(vault_root) - stat = md_file.stat() + relative = fpath.relative_to(vault_root) + stat = fpath.stat() modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat() - raw = md_file.read_text(encoding="utf-8", errors="replace") - post = frontmatter.loads(raw) + raw = fpath.read_text(encoding="utf-8", errors="replace") - tags = _extract_tags(post) - title = _extract_title(post, md_file) - content_preview = post.content[:200].strip() + tags: List[str] = [] + title = fpath.stem.replace("-", " ").replace("_", " ") + content_preview = raw[:200].strip() + + if ext == ".md": + post = frontmatter.loads(raw) + tags = _extract_tags(post) + title = _extract_title(post, fpath) + content_preview = post.content[:200].strip() files.append({ "path": str(relative).replace("\\", "/"), @@ -80,13 +108,14 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: "content_preview": content_preview, "size": stat.st_size, "modified": modified, + "extension": ext, }) for tag in tags: tag_counts[tag] = tag_counts.get(tag, 0) + 1 except Exception as e: - logger.error(f"Error indexing {md_file}: {e}") + logger.error(f"Error indexing {fpath}: {e}") continue logger.info(f"Vault '{vault_name}': indexed {len(files)} files, {len(tag_counts)} unique tags") diff --git a/backend/main.py b/backend/main.py index f79d0eb..3cfc170 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,5 @@ import re +import html as html_mod import logging from pathlib import Path from typing import Optional @@ -7,7 +8,7 @@ import frontmatter import mistune from fastapi import FastAPI, HTTPException, Query from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, PlainTextResponse from backend.indexer import ( build_index, @@ -15,6 +16,7 @@ from backend.indexer import ( index, get_vault_data, find_file_in_index, + SUPPORTED_EXTENSIONS, ) from backend.search import search, get_all_tags @@ -111,20 +113,26 @@ async def api_browse(vault_name: str, path: str = ""): continue rel = str(entry.relative_to(vault_root)).replace("\\", "/") if entry.is_dir(): - # Count .md files recursively - md_count = sum(1 for _ in entry.rglob("*.md")) + # Count all supported files recursively (skip hidden dirs) + file_count = sum( + 1 for f in entry.rglob("*") + if f.is_file() + and not any(p.startswith(".") for p in f.relative_to(entry).parts) + and (f.suffix.lower() in SUPPORTED_EXTENSIONS or f.name.lower() in ("dockerfile", "makefile")) + ) items.append({ "name": entry.name, "path": rel, "type": "directory", - "children_count": md_count, + "children_count": file_count, }) - elif entry.suffix.lower() == ".md": + elif entry.suffix.lower() in SUPPORTED_EXTENSIONS or entry.name.lower() in ("dockerfile", "makefile"): items.append({ "name": entry.name, "path": rel, "type": "file", "size": entry.stat().st_size, + "extension": entry.suffix.lower(), }) except PermissionError: raise HTTPException(status_code=403, detail="Permission denied") @@ -132,9 +140,26 @@ async def api_browse(vault_name: str, path: str = ""): return {"vault": vault_name, "path": path, "items": items} -@app.get("/api/file/{vault_name}") -async def api_file(vault_name: str, path: str = Query(..., description="Relative path to .md file")): - """Return rendered HTML + metadata for a markdown file.""" +# Map file extensions to highlight.js language hints +EXT_TO_LANG = { + ".py": "python", ".js": "javascript", ".ts": "typescript", + ".jsx": "jsx", ".tsx": "tsx", ".sh": "bash", ".bash": "bash", + ".zsh": "bash", ".fish": "fish", ".bat": "batch", ".cmd": "batch", + ".ps1": "powershell", ".json": "json", ".yaml": "yaml", ".yml": "yaml", + ".toml": "toml", ".xml": "xml", ".csv": "plaintext", + ".cfg": "ini", ".ini": "ini", ".conf": "ini", ".env": "bash", + ".html": "html", ".css": "css", ".scss": "scss", ".less": "less", + ".java": "java", ".c": "c", ".cpp": "cpp", ".h": "c", ".hpp": "cpp", + ".cs": "csharp", ".go": "go", ".rs": "rust", ".rb": "ruby", + ".php": "php", ".sql": "sql", ".r": "r", ".swift": "swift", + ".kt": "kotlin", ".txt": "plaintext", ".log": "plaintext", + ".dockerfile": "dockerfile", ".makefile": "makefile", ".cmake": "cmake", +} + + +@app.get("/api/file/{vault_name}/raw") +async def api_file_raw(vault_name: str, path: str = Query(..., description="Relative path to file")): + """Return raw file content.""" vault_data = get_vault_data(vault_name) if not vault_data: raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") @@ -146,30 +171,88 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative raise HTTPException(status_code=404, detail=f"File not found: {path}") raw = file_path.read_text(encoding="utf-8", errors="replace") - post = frontmatter.loads(raw) + return {"vault": vault_name, "path": path, "raw": raw} - # Extract metadata - tags = post.metadata.get("tags", []) - if isinstance(tags, str): - tags = [t.strip().lstrip("#") for t in tags.split(",") if t.strip()] - elif isinstance(tags, list): - tags = [str(t).strip().lstrip("#") for t in tags] + +@app.get("/api/file/{vault_name}/download") +async def api_file_download(vault_name: str, path: str = Query(..., description="Relative path to file")): + """Download a file as attachment.""" + vault_data = get_vault_data(vault_name) + if not vault_data: + raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") + + vault_root = Path(vault_data["path"]) + file_path = vault_root / path + + if not file_path.exists() or not file_path.is_file(): + raise HTTPException(status_code=404, detail=f"File not found: {path}") + + return FileResponse( + path=str(file_path), + filename=file_path.name, + media_type="application/octet-stream", + ) + + +@app.get("/api/file/{vault_name}") +async def api_file(vault_name: str, path: str = Query(..., description="Relative path to file")): + """Return rendered HTML + metadata for a file.""" + vault_data = get_vault_data(vault_name) + if not vault_data: + raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") + + vault_root = Path(vault_data["path"]) + file_path = vault_root / path + + if not file_path.exists() or not file_path.is_file(): + raise HTTPException(status_code=404, detail=f"File not found: {path}") + + raw = file_path.read_text(encoding="utf-8", errors="replace") + ext = file_path.suffix.lower() + + if ext == ".md": + post = frontmatter.loads(raw) + + # Extract metadata + tags = post.metadata.get("tags", []) + if isinstance(tags, str): + tags = [t.strip().lstrip("#") for t in tags.split(",") if t.strip()] + elif isinstance(tags, list): + tags = [str(t).strip().lstrip("#") for t in tags] + else: + tags = [] + + title = post.metadata.get("title", file_path.stem.replace("-", " ").replace("_", " ")) + html_content = _render_markdown(post.content, vault_name) + + return { + "vault": vault_name, + "path": path, + "title": str(title), + "tags": tags, + "frontmatter": dict(post.metadata) if post.metadata else {}, + "html": html_content, + "raw_length": len(raw), + "extension": ext, + "is_markdown": True, + } else: - tags = [] + # Non-markdown: wrap in syntax-highlighted code block + lang = EXT_TO_LANG.get(ext, "plaintext") + escaped = html_mod.escape(raw) + html_content = f'
{escaped}
' - title = post.metadata.get("title", file_path.stem.replace("-", " ").replace("_", " ")) - - html_content = _render_markdown(post.content, vault_name) - - return { - "vault": vault_name, - "path": path, - "title": str(title), - "tags": tags, - "frontmatter": dict(post.metadata) if post.metadata else {}, - "html": html_content, - "raw_length": len(raw), - } + return { + "vault": vault_name, + "path": path, + "title": file_path.name, + "tags": [], + "frontmatter": {}, + "html": html_content, + "raw_length": len(raw), + "extension": ext, + "is_markdown": False, + } @app.get("/api/search") diff --git a/frontend/app.js b/frontend/app.js index 7eddb8c..14d3c69 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -9,6 +9,60 @@ let currentVault = null; let currentPath = null; let searchTimeout = null; + let showingSource = false; + let cachedRawSource = null; + let allVaults = []; + let selectedContextVault = "all"; + + // --------------------------------------------------------------------------- + // 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 @@ -66,11 +120,84 @@ 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"); + filter.addEventListener("change", async () => { + selectedContextVault = filter.value; + showingSource = false; + cachedRawSource = null; + await refreshSidebarForContext(); + await refreshTagsForContext(); + showWelcome(); + }); + } + + 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 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"); container.innerHTML = ""; @@ -156,11 +283,16 @@ } }); } 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("file-text", 16), - document.createTextNode(` ${item.name.replace(/\.md$/i, "")}`), + icon(fileIconName, 16), + document.createTextNode(` ${displayName}`), ]); - fileItem.addEventListener("click", () => openFile(vaultName, item.path)); + fileItem.addEventListener("click", () => { + openFile(vaultName, item.path); + closeMobileSidebar(); + }); container.appendChild(fileItem); } }); @@ -168,15 +300,74 @@ 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 tags = data.tags; const counts = Object.values(tags); if (counts.length === 0) return; @@ -198,7 +389,8 @@ function searchByTag(tag) { const input = document.getElementById("search-input"); input.value = ""; - performSearch("", "all", tag); + const vault = document.getElementById("vault-filter").value; + performSearch("", vault, tag); } // --------------------------------------------------------------------------- @@ -207,12 +399,16 @@ 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="${filePath}"]`; - const active = document.querySelector(selector); - if (active) active.classList.add("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); @@ -248,15 +444,38 @@ tagsDiv.appendChild(t); }); - // Copy path button - const copyBtn = el("button", { class: "btn-copy-path" }, [document.createTextNode("Copier le chemin")]); + // 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.textContent = "Copié !"; - setTimeout(() => (copyBtn.textContent = "Copier le chemin"), 1500); + 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); + }); + // Frontmatter let fmSection = null; if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { @@ -273,20 +492,48 @@ fmSection = el("div", {}, [fmToggle, fmContent]); } - // Markdown content - const mdDiv = el("div", { class: "md-content" }); + // 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]), + el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn]), ])); if (fmSection) area.appendChild(fmSection); area.appendChild(mdDiv); + area.appendChild(rawDiv); // Highlight code blocks area.querySelectorAll("pre code").forEach((block) => { @@ -303,6 +550,7 @@ }); }); + safeCreateIcons(); area.scrollTop = 0; } @@ -375,6 +623,89 @@ 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 // --------------------------------------------------------------------------- @@ -430,6 +761,11 @@ initTheme(); document.getElementById("theme-toggle").addEventListener("click", toggleTheme); initSearch(); + initMobile(); + initVaultContext(); + initSidebarFilter(); + initSidebarResize(); + initTagResize(); try { await Promise.all([loadVaults(), loadTags()]); diff --git a/frontend/index.html b/frontend/index.html index cba25eb..9bc0249 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,6 +15,10 @@
+ +