first commit

This commit is contained in:
2026-03-21 09:52:44 -04:00
commit e76e9ea962
14 changed files with 1848 additions and 0 deletions
+444
View File
@@ -0,0 +1,444 @@
/* ObsiGate — Vanilla JS SPA */
(function () {
"use strict";
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let currentVault = null;
let currentPath = null;
let searchTimeout = null;
// ---------------------------------------------------------------------------
// 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);
const icon = document.getElementById("theme-icon");
if (icon) {
icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun");
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");
}
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
async function api(path) {
const res = await fetch(path);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
// ---------------------------------------------------------------------------
// Sidebar — Vault tree
// ---------------------------------------------------------------------------
async function loadVaults() {
const vaults = await api("/api/vaults");
const container = document.getElementById("vault-tree");
const filter = document.getElementById("vault-filter");
container.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);
});
safeCreateIcons();
}
async function toggleVault(itemEl, vaultName) {
const childContainer = document.getElementById(`vault-children-${vaultName}`);
if (!childContainer) return;
if (childContainer.classList.contains("collapsed")) {
// 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 = "";
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),
]);
container.appendChild(dirItem);
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
container.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 fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [
icon("file-text", 16),
document.createTextNode(` ${item.name.replace(/\.md$/i, "")}`),
]);
fileItem.addEventListener("click", () => openFile(vaultName, item.path));
container.appendChild(fileItem);
}
});
safeCreateIcons();
}
// ---------------------------------------------------------------------------
// Tags
// ---------------------------------------------------------------------------
async function loadTags() {
const data = await api("/api/tags");
const cloud = document.getElementById("tag-cloud");
cloud.innerHTML = "";
const tags = data.tags;
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 searchByTag(tag) {
const input = document.getElementById("search-input");
input.value = "";
performSearch("", "all", tag);
}
// ---------------------------------------------------------------------------
// File viewer
// ---------------------------------------------------------------------------
async function openFile(vaultName, filePath) {
currentVault = vaultName;
currentPath = filePath;
// 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 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);
});
// Copy path button
const copyBtn = el("button", { class: "btn-copy-path" }, [document.createTextNode("Copier le chemin")]);
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => {
copyBtn.textContent = "Copié !";
setTimeout(() => (copyBtn.textContent = "Copier le chemin"), 1500);
});
});
// 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]);
}
// Markdown content
const mdDiv = el("div", { class: "md-content" });
mdDiv.innerHTML = data.html;
// 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]),
]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
// 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);
});
});
area.scrollTop = 0;
}
// ---------------------------------------------------------------------------
// 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;
if (q.length > 0) {
performSearch(q, vault, null);
} else {
showWelcome();
}
}, 300);
});
}
async function performSearch(query, vaultFilter, tagFilter) {
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 = el("div", { class: "search-results-header" });
if (query) {
header.textContent = `${data.count} résultat(s) pour "${query}"`;
} else if (tagFilter) {
header.textContent = `${data.count} fichier(s) avec le tag #${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) => {
tagsDiv.appendChild(el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]));
});
item.appendChild(tagsDiv);
}
item.addEventListener("click", () => openFile(r.vault, r.path));
container.appendChild(item);
});
area.appendChild(container);
}
// ---------------------------------------------------------------------------
// 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 = `
<div class="welcome">
<i data-lucide="library" style="width:48px;height:48px;color:var(--text-muted)"></i>
<h2>ObsiGate</h2>
<p>Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.</p>
</div>`;
safeCreateIcons();
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
async function init() {
initTheme();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
initSearch();
try {
await Promise.all([loadVaults(), loadTags()]);
} catch (err) {
console.error("Failed to initialize ObsiGate:", err);
}
safeCreateIcons();
}
document.addEventListener("DOMContentLoaded", init);
})();
+67
View File
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ObsiGate</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-theme-dark">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-theme-light" disabled>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/umd/lucide.min.js"></script>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="header">
<div class="header-logo">
<i data-lucide="book-open" style="width:20px;height:20px"></i>
ObsiGate
</div>
<div class="search-wrapper">
<i data-lucide="search" class="search-icon" style="width:16px;height:16px"></i>
<input type="text" id="search-input" placeholder="Recherche..." autocomplete="off">
</div>
<select id="vault-filter" class="search-vault-filter">
<option value="all">Toutes les vaults</option>
</select>
<button class="theme-toggle" id="theme-toggle" title="Changer le thème">
<i data-lucide="moon" id="theme-icon" style="width:18px;height:18px"></i>
</button>
</header>
<!-- Main -->
<div class="main-body">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-tree" id="sidebar-tree">
<div class="sidebar-section-title">Vaults</div>
<div id="vault-tree"></div>
</div>
<div class="tag-cloud-section">
<div class="tag-cloud-title">Tags</div>
<div class="tag-cloud" id="tag-cloud"></div>
</div>
</aside>
<!-- Content -->
<main class="content-area" id="content-area">
<div class="welcome" id="welcome">
<i data-lucide="library" style="width:48px;height:48px;color:var(--text-muted)"></i>
<h2>ObsiGate</h2>
<p>Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.</p>
</div>
</main>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>
+621
View File
@@ -0,0 +1,621 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,400;0,600;1,400&display=swap');
/* ===== RESET ===== */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ===== THEME — DARK (default) ===== */
:root[data-theme="dark"] {
--bg-primary: #0f1117;
--bg-secondary: #161b22;
--bg-sidebar: #13161d;
--bg-hover: #1f2430;
--border: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #484f58;
--accent: #58a6ff;
--accent-green: #3fb950;
--tag-bg: #1f2a3a;
--tag-text: #79c0ff;
--code-bg: #161b22;
--search-bg: #21262d;
--scrollbar: #30363d;
}
/* ===== THEME — LIGHT ===== */
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-sidebar: #f0f2f5;
--bg-hover: #e8ecf0;
--border: #d0d7de;
--text-primary: #1f2328;
--text-secondary: #57606a;
--text-muted: #9198a1;
--accent: #0969da;
--accent-green: #1a7f37;
--tag-bg: #ddf4ff;
--tag-text: #0969da;
--code-bg: #f6f8fa;
--search-bg: #ffffff;
--scrollbar: #d0d7de;
}
/* ===== BASE ===== */
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
font-family: 'Lora', Georgia, serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.7;
transition: background 200ms ease, color 200ms ease;
overflow: hidden;
height: 100vh;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* ===== LAYOUT ===== */
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* --- Header --- */
.header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
z-index: 100;
transition: background 200ms ease;
}
.header-logo {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 1.15rem;
color: var(--accent);
white-space: nowrap;
margin-right: auto;
display: flex;
align-items: center;
gap: 8px;
}
.search-wrapper {
flex: 1;
max-width: 520px;
position: relative;
}
.search-wrapper input {
width: 100%;
padding: 8px 14px 8px 38px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--search-bg);
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
outline: none;
transition: border-color 200ms ease, background 200ms ease;
}
.search-wrapper input:focus {
border-color: var(--accent);
}
.search-wrapper .search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
.search-vault-filter {
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--search-bg);
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
outline: none;
cursor: pointer;
}
.theme-toggle {
background: none;
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
color: var(--text-secondary);
display: flex;
align-items: center;
transition: color 200ms ease, border-color 200ms ease;
}
.theme-toggle:hover {
color: var(--accent);
border-color: var(--accent);
}
/* --- Main body --- */
.main-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* --- Sidebar --- */
.sidebar {
width: 280px;
min-width: 280px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: background 200ms ease;
}
.sidebar-tree {
flex: 1;
overflow-y: auto;
padding: 12px 0;
}
.sidebar-tree::-webkit-scrollbar {
width: 6px;
}
.sidebar-tree::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border-radius: 3px;
}
.sidebar-section-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
padding: 8px 16px 4px;
}
.tree-item {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
color: var(--text-secondary);
cursor: pointer;
border-radius: 0;
transition: background 120ms ease, color 120ms ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tree-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-item.active {
background: var(--bg-hover);
color: var(--accent);
}
.tree-item .icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-muted);
}
.tree-item.active .icon {
color: var(--accent);
}
.tree-item.vault-item .icon {
color: var(--accent-green);
}
.tree-children {
padding-left: 16px;
}
.tree-children.collapsed {
display: none;
}
/* --- Tag Cloud --- */
.tag-cloud-section {
border-top: 1px solid var(--border);
padding: 12px 16px;
max-height: 180px;
overflow-y: auto;
}
.tag-cloud-section::-webkit-scrollbar {
width: 6px;
}
.tag-cloud-section::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border-radius: 3px;
}
.tag-cloud-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 8px;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag-item {
display: inline-block;
padding: 2px 8px;
background: var(--tag-bg);
color: var(--tag-text);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
cursor: pointer;
transition: opacity 150ms ease;
line-height: 1.4;
}
.tag-item:hover {
opacity: 0.8;
}
/* --- Content Area --- */
.content-area {
flex: 1;
overflow-y: auto;
padding: 28px 40px 60px;
transition: background 200ms ease;
}
.content-area::-webkit-scrollbar {
width: 8px;
}
.content-area::-webkit-scrollbar-thumb {
background: var(--scrollbar);
border-radius: 4px;
}
/* Welcome */
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
gap: 12px;
}
.welcome h2 {
font-family: 'JetBrains Mono', monospace;
font-size: 1.6rem;
color: var(--text-secondary);
}
.welcome p {
font-size: 0.95rem;
max-width: 400px;
}
/* Breadcrumb */
.breadcrumb {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
margin-bottom: 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.breadcrumb span {
cursor: pointer;
color: var(--text-secondary);
transition: color 120ms ease;
}
.breadcrumb span:hover {
color: var(--accent);
}
.breadcrumb .sep {
cursor: default;
color: var(--text-muted);
}
.breadcrumb .sep:hover {
color: var(--text-muted);
}
/* File header */
.file-header {
margin-bottom: 20px;
}
.file-title {
font-family: 'JetBrains Mono', monospace;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 8px;
}
.file-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 6px;
}
.file-tag {
display: inline-block;
padding: 2px 8px;
background: var(--tag-bg);
color: var(--tag-text);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
cursor: pointer;
}
.file-actions {
margin-top: 8px;
}
.btn-copy-path {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
.btn-copy-path:hover {
color: var(--accent);
border-color: var(--accent);
}
/* Frontmatter collapsible */
.frontmatter-toggle {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
margin-bottom: 12px;
user-select: none;
}
.frontmatter-toggle:hover {
color: var(--text-secondary);
}
.frontmatter-content {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-secondary);
white-space: pre-wrap;
display: none;
}
.frontmatter-content.open {
display: block;
}
/* --- Markdown Rendered Content --- */
.md-content h1, .md-content h2, .md-content h3,
.md-content h4, .md-content h5, .md-content h6 {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
margin: 1.4em 0 0.5em;
color: var(--text-primary);
}
.md-content h1 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.md-content h2 { font-size: 1.25rem; }
.md-content h3 { font-size: 1.1rem; }
.md-content p {
margin: 0.6em 0;
}
.md-content ul, .md-content ol {
padding-left: 1.6em;
margin: 0.5em 0;
}
.md-content li {
margin: 0.25em 0;
}
.md-content blockquote {
border-left: 3px solid var(--accent);
padding: 4px 16px;
margin: 0.8em 0;
color: var(--text-secondary);
background: var(--bg-secondary);
border-radius: 0 6px 6px 0;
}
.md-content code {
font-family: 'JetBrains Mono', monospace;
background: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.88em;
border: 1px solid var(--border);
}
.md-content pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 0.8em 0;
}
.md-content pre code {
background: none;
border: none;
padding: 0;
font-size: 0.85rem;
line-height: 1.55;
}
.md-content table {
width: 100%;
border-collapse: collapse;
margin: 0.8em 0;
font-size: 0.92rem;
}
.md-content th, .md-content td {
border: 1px solid var(--border);
padding: 8px 12px;
text-align: left;
}
.md-content th {
background: var(--bg-secondary);
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
font-size: 0.85rem;
}
.md-content img {
max-width: 100%;
border-radius: 8px;
margin: 0.6em 0;
}
.md-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5em 0;
}
.md-content .task-list-item {
list-style: none;
margin-left: -1.2em;
}
.md-content .task-list-item input[type="checkbox"] {
margin-right: 6px;
}
/* Wikilinks */
.wikilink {
color: var(--accent);
border-bottom: 1px dashed var(--accent);
cursor: pointer;
}
.wikilink:hover {
opacity: 0.8;
}
.wikilink-missing {
color: var(--text-muted);
border-bottom: 1px dashed var(--text-muted);
cursor: default;
}
/* --- Search Results --- */
.search-results {
padding: 0;
}
.search-results-header {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 16px;
}
.search-result-item {
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 10px;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.search-result-item:hover {
background: var(--bg-hover);
border-color: var(--accent);
}
.search-result-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.92rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.search-result-vault {
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
color: var(--accent-green);
margin-bottom: 4px;
}
.search-result-snippet {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.5;
}
.search-result-tags {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.search-result-tags .file-tag {
font-size: 0.7rem;
padding: 1px 6px;
}
/* --- Responsive --- */
@media (max-width: 768px) {
.sidebar {
width: 220px;
min-width: 220px;
}
.content-area {
padding: 20px;
}
}
@media (max-width: 600px) {
.sidebar {
display: none;
}
.content-area {
padding: 16px;
}
}