Expand file type support beyond Markdown to include code, config, and build files with syntax highlighting and improved UI filtering
This commit is contained in:
parent
8a22c1db28
commit
2ed5f65a7a
@ -15,6 +15,18 @@ index: Dict[str, Dict[str, Any]] = {}
|
|||||||
# Vault config: {name: path}
|
# Vault config: {name: path}
|
||||||
vault_config: Dict[str, str] = {}
|
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]:
|
def load_vault_config() -> Dict[str, str]:
|
||||||
"""Read VAULT_N_NAME / VAULT_N_PATH env vars and return {name: path}."""
|
"""Read VAULT_N_NAME / VAULT_N_PATH env vars and return {name: path}."""
|
||||||
@ -60,17 +72,33 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]:
|
|||||||
logger.warning(f"Vault path does not exist: {vault_path}")
|
logger.warning(f"Vault path does not exist: {vault_path}")
|
||||||
return {"files": [], "tags": {}, "path": 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:
|
try:
|
||||||
relative = md_file.relative_to(vault_root)
|
relative = fpath.relative_to(vault_root)
|
||||||
stat = md_file.stat()
|
stat = fpath.stat()
|
||||||
modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
|
modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
|
||||||
|
|
||||||
raw = md_file.read_text(encoding="utf-8", errors="replace")
|
raw = fpath.read_text(encoding="utf-8", errors="replace")
|
||||||
post = frontmatter.loads(raw)
|
|
||||||
|
|
||||||
|
tags: List[str] = []
|
||||||
|
title = fpath.stem.replace("-", " ").replace("_", " ")
|
||||||
|
content_preview = raw[:200].strip()
|
||||||
|
|
||||||
|
if ext == ".md":
|
||||||
|
post = frontmatter.loads(raw)
|
||||||
tags = _extract_tags(post)
|
tags = _extract_tags(post)
|
||||||
title = _extract_title(post, md_file)
|
title = _extract_title(post, fpath)
|
||||||
content_preview = post.content[:200].strip()
|
content_preview = post.content[:200].strip()
|
||||||
|
|
||||||
files.append({
|
files.append({
|
||||||
@ -80,13 +108,14 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]:
|
|||||||
"content_preview": content_preview,
|
"content_preview": content_preview,
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"modified": modified,
|
"modified": modified,
|
||||||
|
"extension": ext,
|
||||||
})
|
})
|
||||||
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error indexing {md_file}: {e}")
|
logger.error(f"Error indexing {fpath}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"Vault '{vault_name}': indexed {len(files)} files, {len(tag_counts)} unique tags")
|
logger.info(f"Vault '{vault_name}': indexed {len(files)} files, {len(tag_counts)} unique tags")
|
||||||
|
|||||||
101
backend/main.py
101
backend/main.py
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import html as html_mod
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -7,7 +8,7 @@ import frontmatter
|
|||||||
import mistune
|
import mistune
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, PlainTextResponse
|
||||||
|
|
||||||
from backend.indexer import (
|
from backend.indexer import (
|
||||||
build_index,
|
build_index,
|
||||||
@ -15,6 +16,7 @@ from backend.indexer import (
|
|||||||
index,
|
index,
|
||||||
get_vault_data,
|
get_vault_data,
|
||||||
find_file_in_index,
|
find_file_in_index,
|
||||||
|
SUPPORTED_EXTENSIONS,
|
||||||
)
|
)
|
||||||
from backend.search import search, get_all_tags
|
from backend.search import search, get_all_tags
|
||||||
|
|
||||||
@ -111,20 +113,26 @@ async def api_browse(vault_name: str, path: str = ""):
|
|||||||
continue
|
continue
|
||||||
rel = str(entry.relative_to(vault_root)).replace("\\", "/")
|
rel = str(entry.relative_to(vault_root)).replace("\\", "/")
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
# Count .md files recursively
|
# Count all supported files recursively (skip hidden dirs)
|
||||||
md_count = sum(1 for _ in entry.rglob("*.md"))
|
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({
|
items.append({
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
"path": rel,
|
"path": rel,
|
||||||
"type": "directory",
|
"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({
|
items.append({
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
"path": rel,
|
"path": rel,
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"size": entry.stat().st_size,
|
"size": entry.stat().st_size,
|
||||||
|
"extension": entry.suffix.lower(),
|
||||||
})
|
})
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
raise HTTPException(status_code=403, detail="Permission denied")
|
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}
|
return {"vault": vault_name, "path": path, "items": items}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/file/{vault_name}")
|
# Map file extensions to highlight.js language hints
|
||||||
async def api_file(vault_name: str, path: str = Query(..., description="Relative path to .md file")):
|
EXT_TO_LANG = {
|
||||||
"""Return rendered HTML + metadata for a markdown file."""
|
".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)
|
vault_data = get_vault_data(vault_name)
|
||||||
if not vault_data:
|
if not vault_data:
|
||||||
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
||||||
@ -146,6 +171,46 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
||||||
|
|
||||||
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
return {"vault": vault_name, "path": path, "raw": raw}
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
post = frontmatter.loads(raw)
|
||||||
|
|
||||||
# Extract metadata
|
# Extract metadata
|
||||||
@ -158,7 +223,6 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
tags = []
|
tags = []
|
||||||
|
|
||||||
title = post.metadata.get("title", file_path.stem.replace("-", " ").replace("_", " "))
|
title = post.metadata.get("title", file_path.stem.replace("-", " ").replace("_", " "))
|
||||||
|
|
||||||
html_content = _render_markdown(post.content, vault_name)
|
html_content = _render_markdown(post.content, vault_name)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -169,6 +233,25 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
"frontmatter": dict(post.metadata) if post.metadata else {},
|
"frontmatter": dict(post.metadata) if post.metadata else {},
|
||||||
"html": html_content,
|
"html": html_content,
|
||||||
"raw_length": len(raw),
|
"raw_length": len(raw),
|
||||||
|
"extension": ext,
|
||||||
|
"is_markdown": True,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Non-markdown: wrap in syntax-highlighted code block
|
||||||
|
lang = EXT_TO_LANG.get(ext, "plaintext")
|
||||||
|
escaped = html_mod.escape(raw)
|
||||||
|
html_content = f'<pre><code class="language-{lang}">{escaped}</code></pre>'
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vault": vault_name,
|
||||||
|
"path": path,
|
||||||
|
"title": file_path.name,
|
||||||
|
"tags": [],
|
||||||
|
"frontmatter": {},
|
||||||
|
"html": html_content,
|
||||||
|
"raw_length": len(raw),
|
||||||
|
"extension": ext,
|
||||||
|
"is_markdown": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
362
frontend/app.js
362
frontend/app.js
@ -9,6 +9,60 @@
|
|||||||
let currentVault = null;
|
let currentVault = null;
|
||||||
let currentPath = null;
|
let currentPath = null;
|
||||||
let searchTimeout = 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
|
// Safe CDN helpers
|
||||||
@ -66,11 +120,84 @@
|
|||||||
return res.json();
|
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
|
// Sidebar — Vault tree
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function loadVaults() {
|
async function loadVaults() {
|
||||||
const vaults = await api("/api/vaults");
|
const vaults = await api("/api/vaults");
|
||||||
|
allVaults = vaults;
|
||||||
const container = document.getElementById("vault-tree");
|
const container = document.getElementById("vault-tree");
|
||||||
const filter = document.getElementById("vault-filter");
|
const filter = document.getElementById("vault-filter");
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
@ -156,11 +283,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} 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 }, [
|
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [
|
||||||
icon("file-text", 16),
|
icon(fileIconName, 16),
|
||||||
document.createTextNode(` ${item.name.replace(/\.md$/i, "")}`),
|
document.createTextNode(` ${displayName}`),
|
||||||
]);
|
]);
|
||||||
fileItem.addEventListener("click", () => openFile(vaultName, item.path));
|
fileItem.addEventListener("click", () => {
|
||||||
|
openFile(vaultName, item.path);
|
||||||
|
closeMobileSidebar();
|
||||||
|
});
|
||||||
container.appendChild(fileItem);
|
container.appendChild(fileItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -168,15 +300,74 @@
|
|||||||
safeCreateIcons();
|
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
|
// Tags
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function loadTags() {
|
async function loadTags() {
|
||||||
const data = await api("/api/tags");
|
const data = await api("/api/tags");
|
||||||
|
renderTagCloud(data.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagCloud(tags) {
|
||||||
const cloud = document.getElementById("tag-cloud");
|
const cloud = document.getElementById("tag-cloud");
|
||||||
cloud.innerHTML = "";
|
cloud.innerHTML = "";
|
||||||
|
|
||||||
const tags = data.tags;
|
|
||||||
const counts = Object.values(tags);
|
const counts = Object.values(tags);
|
||||||
if (counts.length === 0) return;
|
if (counts.length === 0) return;
|
||||||
|
|
||||||
@ -198,7 +389,8 @@
|
|||||||
function searchByTag(tag) {
|
function searchByTag(tag) {
|
||||||
const input = document.getElementById("search-input");
|
const input = document.getElementById("search-input");
|
||||||
input.value = "";
|
input.value = "";
|
||||||
performSearch("", "all", tag);
|
const vault = document.getElementById("vault-filter").value;
|
||||||
|
performSearch("", vault, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -207,12 +399,16 @@
|
|||||||
async function openFile(vaultName, filePath) {
|
async function openFile(vaultName, filePath) {
|
||||||
currentVault = vaultName;
|
currentVault = vaultName;
|
||||||
currentPath = filePath;
|
currentPath = filePath;
|
||||||
|
showingSource = false;
|
||||||
|
cachedRawSource = null;
|
||||||
|
|
||||||
// Highlight active
|
// Highlight active
|
||||||
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
|
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
|
||||||
const selector = `.tree-item[data-vault="${vaultName}"][data-path="${filePath}"]`;
|
const selector = `.tree-item[data-vault="${vaultName}"][data-path="${CSS.escape(filePath)}"]`;
|
||||||
|
try {
|
||||||
const active = document.querySelector(selector);
|
const active = document.querySelector(selector);
|
||||||
if (active) active.classList.add("active");
|
if (active) active.classList.add("active");
|
||||||
|
} catch (e) { /* selector might fail on special chars */ }
|
||||||
|
|
||||||
const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
|
const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
|
||||||
const data = await api(url);
|
const data = await api(url);
|
||||||
@ -248,15 +444,38 @@
|
|||||||
tagsDiv.appendChild(t);
|
tagsDiv.appendChild(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy path button
|
// Action buttons
|
||||||
const copyBtn = el("button", { class: "btn-copy-path" }, [document.createTextNode("Copier le chemin")]);
|
const copyBtn = el("button", { class: "btn-action", title: "Copier le chemin" }, [
|
||||||
|
icon("copy", 14),
|
||||||
|
document.createTextNode("Copier"),
|
||||||
|
]);
|
||||||
copyBtn.addEventListener("click", () => {
|
copyBtn.addEventListener("click", () => {
|
||||||
navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => {
|
navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => {
|
||||||
copyBtn.textContent = "Copié !";
|
copyBtn.querySelector("span") || (copyBtn.lastChild.textContent = "Copié !");
|
||||||
setTimeout(() => (copyBtn.textContent = "Copier le chemin"), 1500);
|
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
|
// Frontmatter
|
||||||
let fmSection = null;
|
let fmSection = null;
|
||||||
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
|
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
|
||||||
@ -273,20 +492,48 @@
|
|||||||
fmSection = el("div", {}, [fmToggle, fmContent]);
|
fmSection = el("div", {}, [fmToggle, fmContent]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown content
|
// Content container (rendered HTML)
|
||||||
const mdDiv = el("div", { class: "md-content" });
|
const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" });
|
||||||
mdDiv.innerHTML = data.html;
|
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
|
// Assemble
|
||||||
area.innerHTML = "";
|
area.innerHTML = "";
|
||||||
area.appendChild(breadcrumb);
|
area.appendChild(breadcrumb);
|
||||||
area.appendChild(el("div", { class: "file-header" }, [
|
area.appendChild(el("div", { class: "file-header" }, [
|
||||||
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
|
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
|
||||||
tagsDiv,
|
tagsDiv,
|
||||||
el("div", { class: "file-actions" }, [copyBtn]),
|
el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn]),
|
||||||
]));
|
]));
|
||||||
if (fmSection) area.appendChild(fmSection);
|
if (fmSection) area.appendChild(fmSection);
|
||||||
area.appendChild(mdDiv);
|
area.appendChild(mdDiv);
|
||||||
|
area.appendChild(rawDiv);
|
||||||
|
|
||||||
// Highlight code blocks
|
// Highlight code blocks
|
||||||
area.querySelectorAll("pre code").forEach((block) => {
|
area.querySelectorAll("pre code").forEach((block) => {
|
||||||
@ -303,6 +550,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
safeCreateIcons();
|
||||||
area.scrollTop = 0;
|
area.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,6 +623,89 @@
|
|||||||
area.appendChild(container);
|
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
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -430,6 +761,11 @@
|
|||||||
initTheme();
|
initTheme();
|
||||||
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
||||||
initSearch();
|
initSearch();
|
||||||
|
initMobile();
|
||||||
|
initVaultContext();
|
||||||
|
initSidebarFilter();
|
||||||
|
initSidebarResize();
|
||||||
|
initTagResize();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadVaults(), loadTags()]);
|
await Promise.all([loadVaults(), loadTags()]);
|
||||||
|
|||||||
@ -15,6 +15,10 @@
|
|||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
<button class="hamburger-btn" id="hamburger-btn" title="Menu">
|
||||||
|
<i data-lucide="menu" style="width:20px;height:20px"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="header-logo">
|
<div class="header-logo">
|
||||||
<i data-lucide="book-open" style="width:20px;height:20px"></i>
|
<i data-lucide="book-open" style="width:20px;height:20px"></i>
|
||||||
ObsiGate
|
ObsiGate
|
||||||
@ -37,19 +41,34 @@
|
|||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<div class="main-body">
|
<div class="main-body">
|
||||||
|
|
||||||
|
<!-- Mobile overlay -->
|
||||||
|
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<!-- Sidebar filter -->
|
||||||
|
<div class="sidebar-filter">
|
||||||
|
<i data-lucide="filter" class="sidebar-filter-icon" style="width:14px;height:14px"></i>
|
||||||
|
<input type="text" id="sidebar-filter-input" placeholder="Filtrer fichiers et tags..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-tree" id="sidebar-tree">
|
<div class="sidebar-tree" id="sidebar-tree">
|
||||||
<div class="sidebar-section-title">Vaults</div>
|
<div class="sidebar-section-title">Vaults</div>
|
||||||
<div id="vault-tree"></div>
|
<div id="vault-tree"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-cloud-section">
|
<!-- Tag resize handle -->
|
||||||
|
<div class="tag-resize-handle" id="tag-resize-handle"></div>
|
||||||
|
|
||||||
|
<div class="tag-cloud-section" id="tag-cloud-section">
|
||||||
<div class="tag-cloud-title">Tags</div>
|
<div class="tag-cloud-title">Tags</div>
|
||||||
<div class="tag-cloud" id="tag-cloud"></div>
|
<div class="tag-cloud" id="tag-cloud"></div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Sidebar resize handle -->
|
||||||
|
<div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<main class="content-area" id="content-area">
|
<main class="content-area" id="content-area">
|
||||||
<div class="welcome" id="welcome">
|
<div class="welcome" id="welcome">
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
--code-bg: #161b22;
|
--code-bg: #161b22;
|
||||||
--search-bg: #21262d;
|
--search-bg: #21262d;
|
||||||
--scrollbar: #30363d;
|
--scrollbar: #30363d;
|
||||||
|
--resize-handle: #30363d;
|
||||||
|
--overlay-bg: rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== THEME — LIGHT ===== */
|
/* ===== THEME — LIGHT ===== */
|
||||||
@ -43,6 +45,8 @@
|
|||||||
--code-bg: #f6f8fa;
|
--code-bg: #f6f8fa;
|
||||||
--search-bg: #ffffff;
|
--search-bg: #ffffff;
|
||||||
--scrollbar: #d0d7de;
|
--scrollbar: #d0d7de;
|
||||||
|
--resize-handle: #d0d7de;
|
||||||
|
--overlay-bg: rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== BASE ===== */
|
/* ===== BASE ===== */
|
||||||
@ -89,6 +93,21 @@ a:hover {
|
|||||||
transition: background 200ms ease;
|
transition: background 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hamburger-btn {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.hamburger-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -164,24 +183,74 @@ a:hover {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Mobile overlay --- */
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--overlay-bg);
|
||||||
|
z-index: 199;
|
||||||
|
}
|
||||||
|
.sidebar-overlay.active {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Sidebar --- */
|
/* --- Sidebar --- */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
min-width: 280px;
|
min-width: 200px;
|
||||||
|
max-width: 500px;
|
||||||
background: var(--bg-sidebar);
|
background: var(--bg-sidebar);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: background 200ms ease;
|
transition: background 200ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Sidebar filter --- */
|
||||||
|
.sidebar-filter {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sidebar-filter input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px 6px 32px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--search-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 200ms ease;
|
||||||
|
}
|
||||||
|
.sidebar-filter input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.sidebar-filter-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 22px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree {
|
.sidebar-tree {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tree::-webkit-scrollbar {
|
.sidebar-tree::-webkit-scrollbar {
|
||||||
@ -225,6 +294,9 @@ a:hover {
|
|||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.tree-item.filtered-out {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-item .icon {
|
.tree-item .icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@ -246,13 +318,33 @@ a:hover {
|
|||||||
.tree-children.collapsed {
|
.tree-children.collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.tree-children.filtered-out {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Tag resize handle --- */
|
||||||
|
.tag-resize-handle {
|
||||||
|
height: 5px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
background: transparent;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
}
|
||||||
|
.tag-resize-handle:hover,
|
||||||
|
.tag-resize-handle.active {
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Tag Cloud --- */
|
/* --- Tag Cloud --- */
|
||||||
.tag-cloud-section {
|
.tag-cloud-section {
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
max-height: 180px;
|
height: 180px;
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.tag-cloud-section::-webkit-scrollbar {
|
.tag-cloud-section::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@ -292,13 +384,32 @@ a:hover {
|
|||||||
.tag-item:hover {
|
.tag-item:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
.tag-item.filtered-out {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Sidebar resize handle (horizontal) --- */
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
width: 5px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
background: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.sidebar-resize-handle:hover,
|
||||||
|
.sidebar-resize-handle.active {
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Content Area --- */
|
/* --- Content Area --- */
|
||||||
.content-area {
|
.content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 28px 40px 60px;
|
padding: clamp(16px, 3vw, 40px) clamp(16px, 4vw, 40px) 60px;
|
||||||
transition: background 200ms ease;
|
transition: background 200ms ease;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.content-area::-webkit-scrollbar {
|
.content-area::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -384,8 +495,12 @@ a:hover {
|
|||||||
}
|
}
|
||||||
.file-actions {
|
.file-actions {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.btn-copy-path {
|
.btn-action {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
@ -395,11 +510,19 @@ a:hover {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
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;
|
||||||
}
|
}
|
||||||
.btn-copy-path:hover {
|
.btn-action:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.btn-action.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--tag-bg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Frontmatter collapsible */
|
/* Frontmatter collapsible */
|
||||||
.frontmatter-toggle {
|
.frontmatter-toggle {
|
||||||
@ -432,6 +555,24 @@ a:hover {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Raw source view --- */
|
||||||
|
.raw-source-view {
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.55;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Markdown Rendered Content --- */
|
/* --- Markdown Rendered Content --- */
|
||||||
.md-content h1, .md-content h2, .md-content h3,
|
.md-content h1, .md-content h2, .md-content h3,
|
||||||
.md-content h4, .md-content h5, .md-content h6 {
|
.md-content h4, .md-content h5, .md-content h6 {
|
||||||
@ -600,22 +741,82 @@ a:hover {
|
|||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Responsive --- */
|
/* --- No-select during resize --- */
|
||||||
@media (max-width: 768px) {
|
body.resizing {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
body.resizing-v {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Responsive — Tablet --- */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 220px;
|
width: 240px;
|
||||||
min-width: 220px;
|
min-width: 200px;
|
||||||
}
|
|
||||||
.content-area {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
/* --- Responsive — Mobile --- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hamburger-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-wrapper {
|
||||||
|
max-width: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-vault-filter {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 5px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 200;
|
||||||
|
width: 280px !important;
|
||||||
|
min-width: 280px !important;
|
||||||
|
max-width: 85vw !important;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-area {
|
.content-area {
|
||||||
padding: 16px;
|
padding: 16px 12px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user