Files
ObsiGate/frontend/popout.html
T
bruno f3bfd01f93
CI / lint (push) Successful in 22s
CI / security (push) Successful in 14s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 15s
fix: popout Mermaid rendering order — run before highlight.js
- Mermaid now initializes and renders BEFORE highlight.js processes code blocks
- highlight.js skips .language-mermaid blocks to avoid 'Could not find language' warnings
- Code highlighting runs after Mermaid SVG replaces the code block
- Removes duplicate/conflicting hljs calls
2026-07-24 14:45:13 -04:00

1567 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 - Popout</title>
<script>
document.documentElement.setAttribute('data-theme', localStorage.getItem('obsigate-theme') || 'dark');
</script>
<link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<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>
<script type="importmap">
{
"imports": {
"@codemirror/state": "https://esm.sh/@codemirror/[email protected]"
}
}
</script>
<script type="module">
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { EditorState } from "@codemirror/state";
import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { defaultKeymap, history, historyKeymap } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { searchKeymap, highlightSelectionMatches } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { markdown } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { python } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { javascript } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { html } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { css } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { json } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { xml } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { sql } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { php } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { cpp } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { java } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { rust } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
import { oneDark } from "https://esm.sh/@codemirror/[email protected]?external=@codemirror/state";
const basicSetup = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
])
];
window.CodeMirror = { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap };
</script>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: var(--bg-primary);
}
.popout-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary);
color: var(--text-primary);
z-index: 100;
transition: opacity 0.3s;
}
</style>
</head>
<body class="popup-mode">
<div class="popout-loading" id="loading">Chargement...</div>
<div class="editor-modal" id="editor-modal">
<div class="editor-container">
<div class="editor-header">
<div class="editor-title" id="editor-title">Édition</div>
<div class="editor-actions">
<button class="editor-btn" id="editor-cancel" title="Annuler" aria-label="Annuler">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn primary" id="editor-save" title="Sauvegarder" aria-label="Sauvegarder">
<i data-lucide="check" style="width:16px;height:16px"></i>
</button>
</div>
</div>
<div class="editor-body" id="editor-body"></div>
</div>
</div>
<div class="main-layout" style="height: 100vh; display: flex; position: relative; overflow: hidden;">
<main class="content-area" id="content-area" style="margin-top: 15px; flex: 1; overflow-y: auto; overflow-x: hidden;">
<!-- JavaScript will inject breadcrumb, title, and markdown content here -->
</main>
<div class="right-sidebar-resize-handle" id="right-sidebar-resize-handle"></div>
<aside class="right-sidebar hidden" id="right-sidebar" role="complementary" aria-label="Table des matières">
<div class="right-sidebar-header">
<h2 class="right-sidebar-title">SUR CETTE PAGE</h2>
<button class="right-sidebar-toggle-btn" id="right-sidebar-toggle-btn" type="button" title="Masquer le panneau">
<i data-lucide="chevron-right" style="width:16px;height:16px"></i>
</button>
</div>
<div class="outline-panel" id="outline-panel">
<nav class="outline-list" id="outline-list" role="navigation"></nav>
<div class="outline-empty" id="outline-empty" hidden>
<i data-lucide="list" style="width:24px;height:24px;opacity:0.3"></i>
<span>Aucun titre dans ce document</span>
</div>
</div>
<div class="reading-progress" id="reading-progress">
<div class="reading-progress-bar"><div class="reading-progress-fill" id="reading-progress-fill"></div></div>
<div class="reading-progress-text" id="reading-progress-text">0%</div>
</div>
</aside>
</div>
<script>
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', 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 initTheme() {
const saved = localStorage.getItem('obsigate-theme') || 'dark';
applyTheme(saved);
}
let rightSidebarVisible = false;
let rightSidebarWidth = 280;
let headingsCache = [];
let activeHeadingId = null;
let showingSource = false;
let cachedRawSource = null;
let editorView = null;
let fallbackEditorEl = null;
let editorVault = null;
let editorPath = null;
function el(tag, attrs, children) {
const e = document.createElement(tag);
if (attrs) { for (let k in attrs) { if (k === "class") e.className = attrs[k]; else e.setAttribute(k, attrs[k]); } }
if (children) { children.forEach(c => { if (typeof c === "string") e.appendChild(document.createTextNode(c)); else e.appendChild(c); }); }
return e;
}
function safeCreateIcons() { if (window.lucide) window.lucide.createIcons(); }
// ---- Login form for popout auth redirect ----
function showLoginForm(vault, path) {
const loading = document.getElementById('loading');
loading.innerHTML = `
<div class="popout-login-card" style="text-align:center;max-width:360px;padding:24px;background:var(--bg-secondary);border-radius:8px;box-shadow:0 4px 12px var(--shadow-color)">
<div style="font-size:2rem;margin-bottom:8px">🔐</div>
<h3 style="margin:0 0 4px;color:var(--text-primary)">Authentification requise</h3>
<p style="margin:0 0 16px;color:var(--text-muted);font-size:0.85rem">Connectez-vous pour accéder à ce fichier</p>
<form id="popout-login-form" style="display:flex;flex-direction:column;gap:10px">
<input type="text" id="popout-username" placeholder="Nom d'utilisateur" required autocomplete="username"
style="padding:10px;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:0.9rem">
<input type="password" id="popout-password" placeholder="Mot de passe" required autocomplete="current-password"
style="padding:10px;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:0.9rem">
<div id="popout-login-error" style="color:var(--text-error,#e74c3c);font-size:0.8rem;display:none"></div>
<button type="submit"
style="padding:10px;background:var(--accent-color,#2563eb);color:white;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem">Se connecter</button>
</form>
</div>
`;
document.getElementById('popout-login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('popout-username').value;
const password = document.getElementById('popout-password').value;
const errorEl = document.getElementById('popout-login-error');
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include',
});
if (!res.ok) {
const err = await res.json();
errorEl.textContent = err.detail || 'Identifiants invalides';
errorEl.style.display = 'block';
return;
}
// Login successful — reload the page to try loading the file again
window.location.reload();
} catch (err) {
errorEl.textContent = 'Erreur réseau';
errorEl.style.display = 'block';
}
});
}
// ---- Outline / TOC Manager ----
const OutlineManager = {
/**
* Slugify text to create valid IDs
*/
slugify(text) {
return text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim() || 'heading';
},
/**
* Parse headings from markdown content
*/
parseHeadings() {
const contentArea = document.querySelector('.md-content');
if (!contentArea) return [];
const headings = [];
const h2s = contentArea.querySelectorAll('h2');
const h3s = contentArea.querySelectorAll('h3');
const allHeadings = [...h2s, ...h3s].sort((a, b) => {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
const usedIds = new Map();
allHeadings.forEach((heading) => {
const text = heading.textContent.trim();
if (!text) return;
const level = parseInt(heading.tagName[1]);
let id = this.slugify(text);
// Handle duplicate IDs
if (usedIds.has(id)) {
const count = usedIds.get(id) + 1;
usedIds.set(id, count);
id = `${id}-${count}`;
} else {
usedIds.set(id, 1);
}
// Inject ID into heading if not present
if (!heading.id) {
heading.id = id;
} else {
id = heading.id;
}
headings.push({
id,
level,
text,
element: heading
});
});
return headings;
},
/**
* Render outline list
*/
renderOutline(headings) {
const outlineList = document.getElementById('outline-list');
const outlineEmpty = document.getElementById('outline-empty');
if (!outlineList) return;
outlineList.innerHTML = '';
if (!headings || headings.length === 0) {
outlineList.hidden = true;
if (outlineEmpty) {
outlineEmpty.hidden = false;
safeCreateIcons();
}
return;
}
outlineList.hidden = false;
if (outlineEmpty) outlineEmpty.hidden = true;
headings.forEach((heading) => {
const item = el('a', {
class: `outline-item level-${heading.level}`,
href: `#${heading.id}`,
'data-heading-id': heading.id,
role: 'link'
}, [document.createTextNode(heading.text)]);
item.addEventListener('click', (e) => {
e.preventDefault();
this.scrollToHeading(heading.id);
});
outlineList.appendChild(item);
});
headingsCache = headings;
},
/**
* Scroll to heading with smooth behavior
*/
scrollToHeading(headingId) {
const heading = document.getElementById(headingId);
if (!heading) return;
const contentArea = document.getElementById('content-area');
if (!contentArea) return;
// Calculate offset for fixed header (if any)
const headerHeight = 80;
const headingTop = heading.offsetTop;
contentArea.scrollTo({
top: headingTop - headerHeight,
behavior: 'smooth'
});
// Update active state immediately
this.setActiveHeading(headingId);
},
/**
* Set active heading in outline
*/
setActiveHeading(headingId) {
if (activeHeadingId === headingId) return;
activeHeadingId = headingId;
const items = document.querySelectorAll('.outline-item');
items.forEach((item) => {
if (item.getAttribute('data-heading-id') === headingId) {
item.classList.add('active');
item.setAttribute('aria-current', 'location');
// Scroll outline item into view
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
} else {
item.classList.remove('active');
item.removeAttribute('aria-current');
}
});
},
/**
* Initialize outline for current document
*/
init() {
const headings = this.parseHeadings();
this.renderOutline(headings);
ScrollSpyManager.init(headings);
ReadingProgressManager.init();
},
/**
* Cleanup
*/
destroy() {
ScrollSpyManager.destroy();
ReadingProgressManager.destroy();
headingsCache = [];
activeHeadingId = null;
}
};
const ScrollSpyManager = {
observer: null,
headings: [],
init(headings) {
this.destroy();
this.headings = headings;
if (!headings || headings.length === 0) return;
const contentArea = document.getElementById('content-area');
if (!contentArea) return;
const options = {
root: contentArea,
rootMargin: '-20% 0px -70% 0px',
threshold: [0, 0.3, 0.5, 1.0]
};
this.observer = new IntersectionObserver((entries) => {
// Find the most visible heading
let mostVisible = null;
let maxRatio = 0;
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio > maxRatio) {
maxRatio = entry.intersectionRatio;
mostVisible = entry.target;
}
});
if (mostVisible && mostVisible.id) {
OutlineManager.setActiveHeading(mostVisible.id);
}
}, options);
// Observe all headings
headings.forEach((heading) => {
if (heading.element) {
this.observer.observe(heading.element);
}
});
},
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.headings = [];
}
};
const ReadingProgressManager = {
scrollHandler: null,
init() {
this.destroy();
const contentArea = document.getElementById('content-area');
if (!contentArea) return;
this.scrollHandler = this.throttle(() => {
this.updateProgress();
}, 100);
contentArea.addEventListener('scroll', this.scrollHandler);
this.updateProgress();
},
updateProgress() {
const contentArea = document.getElementById('content-area');
const progressFill = document.getElementById('reading-progress-fill');
const progressText = document.getElementById('reading-progress-text');
if (!contentArea || !progressFill || !progressText) return;
const scrollTop = contentArea.scrollTop;
const scrollHeight = contentArea.scrollHeight;
const clientHeight = contentArea.clientHeight;
const maxScroll = scrollHeight - clientHeight;
const percentage = maxScroll > 0 ? Math.round((scrollTop / maxScroll) * 100) : 0;
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${percentage}%`;
},
throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
},
destroy() {
const contentArea = document.getElementById('content-area');
if (contentArea && this.scrollHandler) {
contentArea.removeEventListener('scroll', this.scrollHandler);
}
this.scrollHandler = null;
// Reset progress
const progressFill = document.getElementById('reading-progress-fill');
const progressText = document.getElementById('reading-progress-text');
if (progressFill) progressFill.style.width = '0%';
if (progressText) progressText.textContent = '0%';
}
};
const RightSidebarManager = {
init() {
this.loadState();
this.initToggle();
this.initResize();
},
loadState() {
const savedVisible = localStorage.getItem('obsigate-right-sidebar-visible');
const savedWidth = localStorage.getItem('obsigate-right-sidebar-width');
if (savedVisible !== null) {
rightSidebarVisible = savedVisible === 'true';
}
if (savedWidth) {
rightSidebarWidth = parseInt(savedWidth) || 280;
}
this.applyState();
},
applyState() {
const sidebar = document.getElementById('right-sidebar');
const handle = document.getElementById('right-sidebar-resize-handle');
const tocBtn = document.getElementById('toc-toggle-btn');
const headerToggleBtn = document.getElementById('right-sidebar-toggle-btn');
if (!sidebar) return;
if (rightSidebarVisible) {
sidebar.classList.remove('hidden');
sidebar.style.width = `${rightSidebarWidth}px`;
if (handle) handle.classList.remove('hidden');
if (tocBtn) {
tocBtn.classList.add('active');
tocBtn.title = 'Masquer le sommaire';
}
if (headerToggleBtn) {
headerToggleBtn.title = 'Masquer le panneau';
headerToggleBtn.setAttribute('aria-label', 'Masquer le panneau');
}
} else {
sidebar.classList.add('hidden');
if (handle) handle.classList.add('hidden');
if (tocBtn) {
tocBtn.classList.remove('active');
tocBtn.title = 'Afficher le sommaire';
}
if (headerToggleBtn) {
headerToggleBtn.title = 'Afficher le panneau';
headerToggleBtn.setAttribute('aria-label', 'Afficher le panneau');
}
}
// Update icons
safeCreateIcons();
},
toggle() {
rightSidebarVisible = !rightSidebarVisible;
localStorage.setItem('obsigate-right-sidebar-visible', rightSidebarVisible);
this.applyState();
},
initToggle() {
const toggleBtn = document.getElementById('right-sidebar-toggle-btn');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggle());
}
},
initResize() {
const handle = document.getElementById('right-sidebar-resize-handle');
const sidebar = document.getElementById('right-sidebar');
if (!handle || !sidebar) return;
let isResizing = false;
let startX = 0;
let startWidth = 0;
const onMouseDown = (e) => {
isResizing = true;
startX = e.clientX;
startWidth = sidebar.offsetWidth;
handle.classList.add('active');
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
};
const onMouseMove = (e) => {
if (!isResizing) return;
const delta = startX - e.clientX;
let newWidth = startWidth + delta;
// Constrain width
newWidth = Math.max(200, Math.min(400, newWidth));
sidebar.style.width = `${newWidth}px`;
rightSidebarWidth = newWidth;
};
const onMouseUp = () => {
if (!isResizing) return;
isResizing = false;
handle.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
localStorage.setItem('obsigate-right-sidebar-width', rightSidebarWidth);
};
handle.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
};
async function initPopout() {
const pathParts = window.location.pathname.split('/');
const vault = decodeURIComponent(pathParts[2]);
const path = decodeURIComponent(pathParts.slice(3).join('/'));
showingSource = false;
cachedRawSource = null;
if (!vault || !path) {
document.getElementById('loading').textContent = "Paramètres invalides";
return;
}
try {
const response = await fetch(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
credentials: 'include'
});
if (response.status === 401) {
showLoginForm(vault, path);
return;
}
if (!response.ok) throw new Error("Erreur lors du chargement (Essayez de vous reconnecter sur le site principal)");
const data = await response.json();
document.title = data.title + " - ObsiGate";
const area = document.getElementById('content-area');
area.innerHTML = "";
// Build breadcrumb (identical structure to app.js)
const parts = path.split("/");
const breadcrumb = document.createElement("div");
breadcrumb.className = "breadcrumb";
breadcrumb.appendChild(createBreadcrumbSep());
breadcrumb.appendChild(createBreadcrumbItem(vault));
parts.forEach((part, i) => {
breadcrumb.appendChild(createBreadcrumbSep());
const name = i === parts.length - 1 ? part.replace(/\.md$/i, "") : part;
breadcrumb.appendChild(createBreadcrumbItem(name));
});
area.appendChild(breadcrumb);
// Build file header
const header = document.createElement("div");
header.className = "file-header";
const titleDiv = document.createElement("div");
titleDiv.className = "file-title";
titleDiv.textContent = data.title;
header.appendChild(titleDiv);
// Tags
if (data.tags && data.tags.length > 0) {
const tagsDiv = document.createElement("div");
tagsDiv.className = "file-tags";
data.tags.forEach(t => {
const span = document.createElement("span");
span.className = "file-tag";
span.textContent = "#" + t;
tagsDiv.appendChild(span);
});
header.appendChild(tagsDiv);
}
// Action buttons
const actionsDiv = document.createElement("div");
actionsDiv.className = "file-actions";
const copyBtn = document.createElement("button");
copyBtn.className = "btn-action";
copyBtn.title = "Copier la source";
copyBtn.innerHTML = '<i data-lucide="copy" style="width:14px;height:14px"></i> Copier';
copyBtn.addEventListener("click", async () => {
try {
const rawUrl = `/api/file/${encodeURIComponent(vault)}/raw?path=${encodeURIComponent(path)}`;
const rawData = await (await fetch(rawUrl, {credentials: 'include'})).json();
await navigator.clipboard.writeText(rawData.raw);
copyBtn.textContent = "Copié !";
setTimeout(() => (copyBtn.innerHTML = '<i data-lucide="copy" style="width:14px;height:14px"></i> Copier'), 1500);
if (window.lucide) window.lucide.createIcons();
} catch (e) {}
});
actionsDiv.appendChild(copyBtn);
const sourceBtn = document.createElement("button");
sourceBtn.className = "btn-action";
sourceBtn.title = "Voir la source";
sourceBtn.innerHTML = '<i data-lucide="code" style="width:14px;height:14px"></i> Source';
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(vault)}/raw?path=${encodeURIComponent(path)}`;
const rawResponse = await fetch(rawUrl, { credentials: 'include' });
if (!rawResponse.ok) throw new Error("Erreur lors du chargement de la source");
const rawData = await rawResponse.json();
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";
}
});
actionsDiv.appendChild(sourceBtn);
const dlBtn = document.createElement("button");
dlBtn.className = "btn-action";
dlBtn.title = "Télécharger en .md";
dlBtn.innerHTML = '<i data-lucide="file-text" style="width:14px;height:14px"></i> .md';
dlBtn.addEventListener("click", () => {
const a = document.createElement("a");
a.href = `/api/file/${encodeURIComponent(vault)}/download?path=${encodeURIComponent(path)}`;
a.download = path.split('/').pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
actionsDiv.appendChild(dlBtn);
const pdfBtn = document.createElement("button");
pdfBtn.className = "btn-action";
pdfBtn.title = "Télécharger en PDF";
pdfBtn.innerHTML = '<i data-lucide="file" style="width:14px;height:14px"></i> PDF';
pdfBtn.addEventListener("click", () => {
window.open(`/api/file/${encodeURIComponent(vault)}/pdf?path=${encodeURIComponent(path)}`, "_blank");
});
actionsDiv.appendChild(pdfBtn);
const editBtn = document.createElement("button");
editBtn.className = "btn-action";
editBtn.title = "Éditer";
editBtn.innerHTML = '<i data-lucide="edit" style="width:14px;height:14px"></i> Éditer';
editBtn.addEventListener("click", () => {
openEditor(vault, path);
});
actionsDiv.appendChild(editBtn);
const tocBtn = document.createElement("button");
tocBtn.className = "btn-action";
tocBtn.id = "toc-toggle-btn";
tocBtn.title = "Afficher/Masquer le sommaire";
tocBtn.innerHTML = '<i data-lucide="list" style="width:14px;height:14px"></i> TOC';
tocBtn.addEventListener("click", () => { RightSidebarManager.toggle(); });
actionsDiv.appendChild(tocBtn);
// Share button
const shareBtn = document.createElement("button");
shareBtn.className = "btn-action";
shareBtn.title = "Partager ce document";
shareBtn.innerHTML = '<i data-lucide="share-2" style="width:14px;height:14px"></i> Partager';
shareBtn.addEventListener("click", async () => {
try {
const res = await fetch(`/api/share/${encodeURIComponent(vault)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
credentials: "include",
});
if (!res.ok) throw new Error("Erreur");
const share = await res.json();
const url = window.location.origin + share.url;
try { await navigator.clipboard.writeText(url); } catch { /* fallback */ }
alert("Lien copié : " + url);
} catch (err) { alert("Erreur: " + err.message); }
});
actionsDiv.appendChild(shareBtn);
// Bookmark button
const bookmarkBtn = document.createElement("button");
bookmarkBtn.className = "btn-action";
bookmarkBtn.title = "Ajouter/Retirer des bookmarks";
bookmarkBtn.innerHTML = '<i data-lucide="bookmark-plus" style="width:14px;height:14px"></i> Bookmark';
bookmarkBtn.addEventListener("click", async () => {
try {
const res = await fetch("/api/bookmarks/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault, path, title: data.title }),
credentials: "include",
});
if (!res.ok) throw new Error("Erreur");
const bm = await res.json();
bookmarkBtn.classList.toggle("active", bm.bookmarked);
} catch (err) { alert("Erreur: " + err.message); }
});
actionsDiv.appendChild(bookmarkBtn);
header.appendChild(actionsDiv);
area.appendChild(header);
// Frontmatter — Accent Card
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
const fmSection = buildFrontmatterCard(data.frontmatter);
area.appendChild(fmSection);
}
// Markdown content wrapper
const mdDiv = document.createElement("div");
mdDiv.className = "md-content";
mdDiv.id = "file-rendered-content";
mdDiv.innerHTML = data.html;
area.appendChild(mdDiv);
const rawDiv = document.createElement("div");
rawDiv.className = "raw-source-view";
rawDiv.id = "file-raw-content";
rawDiv.style.display = "none";
area.appendChild(rawDiv);
// Render lucide icons
if (window.lucide) {
window.lucide.createIcons();
}
OutlineManager.init(); RightSidebarManager.init();
// Mermaid + code highlighting handled below FIRST (before highlight.js)
if (typeof mermaid !== 'undefined') {
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default' });
mermaid.run({ querySelector: '.language-mermaid' }).then(() => {
// After Mermaid renders, highlight remaining code blocks (skip mermaid SVGs)
document.querySelectorAll('pre code:not(.language-mermaid)').forEach((el) => {
if (typeof hljs !== 'undefined') hljs.highlightElement(el);
});
}).catch(() => {
// Mermaid failed — highlight everything else anyway
document.querySelectorAll('pre code:not(.language-mermaid)').forEach((el) => {
if (typeof hljs !== 'undefined') hljs.highlightElement(el);
});
});
} else {
// No mermaid — just highlight code
document.querySelectorAll('pre code').forEach((el) => {
if (typeof hljs !== 'undefined') hljs.highlightElement(el);
});
}
// Hide loading
document.getElementById('loading').style.opacity = '0';
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 300);
} catch (err) {
// Check if this was an auth error from a later fetch
if (err.message && err.message.includes('401')) {
showLoginForm(vault, path);
} else {
document.getElementById('loading').textContent = err.message || 'Erreur de chargement';
}
}
}
async function waitForHljs() {
let attempts = 0;
while (typeof hljs === 'undefined' && attempts < 50) {
await new Promise((resolve) => setTimeout(resolve, 100));
attempts++;
}
}
async function waitForCodeMirror() {
let attempts = 0;
while (!window.CodeMirror && attempts < 50) {
await new Promise((resolve) => setTimeout(resolve, 100));
attempts++;
}
if (!window.CodeMirror) {
throw new Error("CodeMirror failed to load");
}
}
async function openEditor(vaultName, filePath) {
editorVault = vaultName;
editorPath = filePath;
const modal = document.getElementById("editor-modal");
const titleEl = document.getElementById("editor-title");
const bodyEl = document.getElementById("editor-body");
titleEl.textContent = `Édition: ${filePath.split("/").pop()}`;
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
const rawResponse = await fetch(rawUrl, { credentials: 'include' });
if (!rawResponse.ok) {
throw new Error("Erreur lors du chargement du contenu à éditer");
}
const rawData = await rawResponse.json();
cachedRawSource = rawData.raw;
bodyEl.innerHTML = "";
// Create AI toolbar container (before CodeMirror)
const aiContainer = document.createElement('div');
aiContainer.id = 'ai-toolbar-container';
bodyEl.appendChild(aiContainer);
if (editorView) {
editorView.destroy();
editorView = null;
}
fallbackEditorEl = null;
try {
await waitForCodeMirror();
const { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap } = window.CodeMirror;
const currentTheme = document.documentElement.getAttribute("data-theme");
const fileExt = filePath.split(".").pop().toLowerCase();
const extensions = [
basicSetup,
keymap.of([
{
key: "Mod-s",
run: () => {
saveFile();
return true;
},
},
]),
EditorView.lineWrapping,
];
const langMap = {
md: markdown,
markdown: markdown,
py: python,
js: javascript,
jsx: javascript,
ts: javascript,
tsx: javascript,
mjs: javascript,
cjs: javascript,
html: html,
htm: html,
css: css,
scss: css,
less: css,
json: json,
xml: xml,
svg: xml,
sql: sql,
php: php,
cpp: cpp,
cc: cpp,
cxx: cpp,
c: cpp,
h: cpp,
hpp: cpp,
java: java,
rs: rust,
sh: javascript,
bash: javascript,
zsh: javascript,
};
const langMode = langMap[fileExt];
if (langMode) {
extensions.push(langMode());
}
if (currentTheme === "dark") {
extensions.push(oneDark);
}
const state = EditorState.create({
doc: rawData.raw,
extensions: extensions,
});
editorView = new EditorView({
state: state,
parent: bodyEl,
});
} catch (err) {
console.error("CodeMirror init failed, falling back to textarea:", err);
fallbackEditorEl = document.createElement("textarea");
fallbackEditorEl.className = "fallback-editor";
fallbackEditorEl.value = rawData.raw;
bodyEl.appendChild(fallbackEditorEl);
}
modal.classList.add("active");
// Initialize AI toolbar (after editor is ready)
if (window._popoutAIToolbarCreated) {
// Update the existing toolbar's editor reference
window._popoutAIToolbarCreated = false;
}
createPopoutAIToolbar(aiContainer, () => editorView);
safeCreateIcons();
}
function closeEditor() {
const modal = document.getElementById("editor-modal");
modal.classList.remove("active");
if (editorView) {
editorView.destroy();
editorView = null;
}
fallbackEditorEl = null;
editorVault = null;
editorPath = null;
// Clean up AI toolbar
const aiContainer = document.getElementById('ai-toolbar-container');
if (aiContainer) aiContainer.innerHTML = '';
}
// ── AI Toolbar (inline, same as main app's ai.js) ──
async function popoutFetch(url, opts = {}) {
const res = await fetch(url, { ...opts, credentials: 'include' });
if (!res.ok) {
let detail = '';
try { const err = await res.json(); detail = err.detail || ''; } catch {}
throw new Error(detail || `HTTP ${res.status}`);
}
return res.json();
}
async function popoutAiAction(endpoint, text, extra = {}) {
const body = { text, ...extra };
const data = await popoutFetch(`/api/ai/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return data.result;
}
function popoutGetSelection(ev) {
if (!ev) return '';
const { from, to } = ev.state.selection.main;
if (from === to) return ev.state.doc.toString();
return ev.state.doc.sliceString(from, to);
}
function popoutReplaceSelection(ev, newText, mode = 'replace') {
if (!ev) return;
const { from, to } = ev.state.selection.main;
if (mode === 'append') {
ev.dispatch({ changes: { from: to, insert: '\n' + newText }, selection: { anchor: to + 1 + newText.length } });
} else if (mode === 'before') {
ev.dispatch({ changes: { from, insert: newText + '\n' } });
} else {
ev.dispatch({ changes: { from, to, insert: newText }, selection: { anchor: from + newText.length } });
}
}
function popoutShowToast(msg, type) {
const existing = document.querySelector('.popout-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'popout-toast';
const colors = { success: '#22c55e', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b' };
Object.assign(toast.style, {
position: 'fixed', bottom: '16px', right: '16px', zIndex: '9999',
background: 'var(--bg-secondary)', color: 'var(--text-primary)',
padding: '8px 16px', borderRadius: '6px', fontSize: '0.8rem',
borderLeft: `3px solid ${colors[type] || colors.info}`,
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
transition: 'opacity 0.3s', opacity: '1',
});
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 2500);
}
function popoutCreateMenu(items, parentEl) {
const existing = parentEl.querySelector('.ai-dropdown-menu');
if (existing) existing.remove();
const menu = document.createElement('div');
menu.className = 'ai-dropdown-menu';
Object.assign(menu.style, {
position: 'absolute', top: '100%', left: '0',
background: 'var(--bg-primary)', border: '1px solid var(--border-color)',
borderRadius: '6px', padding: '4px 0', minWidth: '200px', zIndex: '100',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
});
items.forEach(item => {
const el = document.createElement('div');
el.className = 'ai-menu-item';
Object.assign(el.style, {
padding: '6px 12px', cursor: 'pointer', display: 'flex', alignItems: 'center',
gap: '8px', fontSize: '0.8rem', color: 'var(--text-primary)',
whiteSpace: 'nowrap', background: 'transparent',
});
el.innerHTML = item.label;
if (item.hint) {
const hint = document.createElement('span');
hint.style.cssText = 'margin-left:auto;font-size:0.65rem;color:var(--text-muted)';
hint.textContent = item.hint;
el.appendChild(hint);
}
el.addEventListener('mouseenter', () => { el.style.background = 'rgba(255,255,255,0.06)'; });
el.addEventListener('mouseleave', () => { el.style.background = 'transparent'; });
el.addEventListener('click', (e) => {
e.stopPropagation();
if (item.action) item.action();
menu.remove();
});
menu.appendChild(el);
});
parentEl.appendChild(menu);
const closeHandler = (e) => {
if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeHandler); }
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
}
function popoutCreateDropdownBtn(text, items, tooltip) {
const btn = document.createElement('button');
btn.className = 'ai-toolbar-btn';
btn.innerHTML = text + ' ▾';
btn.title = tooltip || text;
Object.assign(btn.style, {
position: 'relative', fontSize: '0.7rem', background: 'none',
border: 'none', cursor: 'pointer', padding: '3px 6px',
borderRadius: '4px', color: 'var(--text-primary)',
});
btn.addEventListener('click', (e) => { e.stopPropagation(); popoutCreateMenu(items, btn); });
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.08)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
return btn;
}
async function createPopoutAIToolbar(container, getEditorView) {
if (window._popoutAIToolbarCreated) return;
window._popoutAIToolbarCreated = true;
// Check if AI is configured
let aiConfigured = false;
try {
const status = await popoutFetch('/api/ai/status');
aiConfigured = status.configured;
} catch { aiConfigured = false; }
if (!aiConfigured) {
const hint = document.createElement('div');
hint.style.cssText = 'padding:6px 12px;font-size:0.7rem;color:var(--text-muted);border-bottom:1px solid var(--border-color)';
hint.textContent = 'AI non configuré — ajouter une clé API dans .env';
container.appendChild(hint);
return;
}
function ev() { return getEditorView(); }
const toolbar = document.createElement('div');
toolbar.className = 'ai-toolbar';
Object.assign(toolbar.style, {
display: 'flex', alignItems: 'center', gap: '2px', padding: '4px 8px',
borderBottom: '1px solid var(--border-color)', background: 'var(--bg-secondary)', flexWrap: 'wrap',
position: 'sticky', top: '0', zIndex: '5',
});
// AI inline complete button
const aiBtn = document.createElement('button');
aiBtn.className = 'ai-toolbar-btn';
aiBtn.innerHTML = '✦ AI';
Object.assign(aiBtn.style, {
color: '#60a5fa', fontWeight: '600', fontSize: '0.75rem',
background: 'none', border: 'none', cursor: 'pointer',
padding: '3px 8px', borderRadius: '4px',
});
aiBtn.title = 'AI Inline Completion (Ctrl+J)';
aiBtn.addEventListener('click', async () => {
const v = ev(); if (!v) return;
const text = popoutGetSelection(v);
if (!text.trim()) return;
const origHTML = aiBtn.innerHTML;
aiBtn.innerHTML = '⏳ AI...'; aiBtn.disabled = true;
try {
const result = await popoutAiAction('inline-complete', text);
popoutReplaceSelection(v, result, 'append');
popoutShowToast('AI: complétion ajoutée', 'success');
} catch (e) {
popoutShowToast('AI: ' + (String(e.message).includes('401') ? 'clé API invalide' : e.message), 'error');
} finally {
aiBtn.innerHTML = origHTML; aiBtn.disabled = false;
}
});
async function popoutAction(endpoint, extra = {}, mode = 'replace') {
const v = ev(); if (!v) return;
const text = popoutGetSelection(v);
if (!text.trim()) { popoutShowToast('AI: sélectionnez du texte', 'warning'); return; }
popoutShowToast('⏳ AI: traitement...', 'info');
try {
const result = await popoutAiAction(endpoint, text, extra);
popoutReplaceSelection(v, result, mode);
popoutShowToast('AI: ✓', 'success');
} catch (e) {
const msg = String(e.message);
if (msg.includes('401')) popoutShowToast('AI: clé API invalide', 'error');
else if (msg.includes('402') || msg.includes('429')) popoutShowToast('AI: quota dépassé', 'error');
else popoutShowToast('AI: ' + msg, 'error');
}
}
const sep = (() => { const s = document.createElement('span'); s.style.cssText = 'width:1px;height:16px;background:var(--border-color);margin:0 2px'; return s; });
toolbar.appendChild(aiBtn);
toolbar.appendChild(sep());
toolbar.appendChild(popoutCreateDropdownBtn('Éditer', [
{ label: '🪄 Improve writing', action: () => popoutAction('improve') },
{ label: '🔤 Fix spelling & grammar', action: () => popoutAction('fix-spelling') },
{ label: '📏 Make shorter', action: () => popoutAction('make-shorter') },
{ label: '📐 Make longer', action: () => popoutAction('make-longer') },
{ label: '📋 Simplify language', action: () => popoutAction('simplify') },
], 'Modifie le texte sélectionné'));
toolbar.appendChild(popoutCreateDropdownBtn('Ton', [
{ label: '💼 Professional tone', action: () => popoutAction('tone', { tone: 'professional' }) },
{ label: '💬 Casual tone', action: () => popoutAction('tone', { tone: 'casual' }) },
], 'Change le ton du texte'));
toolbar.appendChild(popoutCreateDropdownBtn('Traduire', [
{ label: '🇬🇧 English', action: () => popoutAction('translate', { target_lang: 'English' }) },
{ label: '🇨🇳 Chinese', action: () => popoutAction('translate', { target_lang: 'Chinese' }) },
{ label: '🇯🇵 Japanese', action: () => popoutAction('translate', { target_lang: 'Japanese' }) },
{ label: '🇩🇪 German', action: () => popoutAction('translate', { target_lang: 'German' }) },
{ label: '🇫🇷 French', action: () => popoutAction('translate', { target_lang: 'French' }) },
{ label: '🇪🇸 Spanish', action: () => popoutAction('translate', { target_lang: 'Spanish' }) },
], 'Traduit le texte sélectionné'));
toolbar.appendChild(sep());
toolbar.appendChild(popoutCreateDropdownBtn('Générer', [
{ label: 'ℹ️ Explain this', action: () => popoutAction('explain', {}, 'append') },
{ label: '📝 Summarize', action: () => popoutAction('summarize', {}, 'append') },
{ label: '✏️ Continue writing', action: () => popoutAction('continue', {}, 'append') },
], 'Génère du contenu'));
const rewriteBtn = document.createElement('button');
rewriteBtn.className = 'ai-toolbar-btn';
rewriteBtn.innerHTML = '💬 Réécrire';
rewriteBtn.title = 'Réécrit selon vos instructions';
Object.assign(rewriteBtn.style, {
fontSize: '0.7rem', background: 'transparent', border: 'none',
cursor: 'pointer', padding: '3px 6px', borderRadius: '4px', color: 'var(--text-primary)',
});
rewriteBtn.addEventListener('click', async () => {
const v = ev(); if (!v) return;
const text = popoutGetSelection(v);
const instruction = prompt('Instruction de réécriture :', '');
if (!instruction) return;
try {
const result = await popoutAiAction('rewrite', text, { instruction });
popoutReplaceSelection(v, result);
popoutShowToast('AI: texte réécrit', 'success');
} catch (e) {
popoutShowToast('AI: ' + (String(e.message).includes('401') ? 'clé API invalide' : e.message), 'error');
}
});
toolbar.appendChild(rewriteBtn);
toolbar.appendChild(popoutCreateDropdownBtn('🧰 Boîte', [
{ label: '📋 Convert to list', action: () => popoutAction('to-list') },
{ label: '📊 Convert to table', action: () => popoutAction('to-table') },
{ label: '⚙️ Generate frontmatter', action: () => popoutAction('frontmatter', {}, 'before') },
{ label: '🔷 Convert to canvas', action: () => popoutAction('to-canvas', {}, 'append') },
], 'Outils de conversion'));
container.innerHTML = ''; // clear previous
container.appendChild(toolbar);
// Ctrl+J keyboard shortcut
document.addEventListener('keydown', function popoutAIKey(e) {
if (e.ctrlKey && e.key === 'j') {
const modal = document.getElementById('editor-modal');
if (!modal || !modal.classList.contains('active')) return;
e.preventDefault();
aiBtn.click();
}
});
}
async function saveFile() {
if ((!editorView && !fallbackEditorEl) || !editorVault || !editorPath) return;
const content = editorView ? editorView.state.doc.toString() : fallbackEditorEl.value;
const saveBtn = document.getElementById("editor-save");
const originalHTML = saveBtn.innerHTML;
try {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
credentials: 'include'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de sauvegarde");
}
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
safeCreateIcons();
closeEditor();
await initPopout();
} catch (err) {
console.error("Save error:", err);
saveBtn.innerHTML = originalHTML;
safeCreateIcons();
alert(err.message || "Erreur lors de la sauvegarde");
} finally {
saveBtn.disabled = false;
}
}
function createBreadcrumbSep() {
const sep = document.createElement("span");
sep.className = "sep";
sep.textContent = " / ";
return sep;
}
function createBreadcrumbItem(text) {
const item = document.createElement("span");
item.className = "breadcrumb-item";
item.textContent = text;
return item;
}
function buildFrontmatterCard(frontmatter) {
// Helper: format date
function formatDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
const date = d.toISOString().slice(0, 10);
const time = d.toTimeString().slice(0, 5);
return `${date} · ${time}`;
}
// Helper: create element
function el(tag, className, children) {
const e = document.createElement(tag);
if (className) e.className = className;
if (children) {
children.forEach(c => { if (c) e.appendChild(c); });
}
return e;
}
// Extract boolean flags
const booleanFlags = ['publish', 'favoris', 'template', 'task', 'archive', 'draft', 'private']
.map(key => ({ key, value: !!frontmatter[key] }));
// Toggle state — collapsed by default
let isOpen = false;
// Build header with chevron
const chevron = el("span", "fm-chevron closed");
chevron.innerHTML = '<i data-lucide="chevron-down" style="width:14px;height:14px"></i>';
const fmHeader = el("div", "fm-header");
fmHeader.appendChild(chevron);
fmHeader.appendChild(document.createTextNode("Frontmatter"));
// ZONE 1: Top strip
const acTop = el("div", "ac-top");
// Title badge
const title = frontmatter.titre || frontmatter.title || '';
if (title) {
const titleSpan = el("span", "ac-title");
titleSpan.textContent = `"${title}"`;
acTop.appendChild(titleSpan);
}
// Status badge
if (frontmatter.statut) {
const statusBadge = el("span", "ac-badge green");
const dot = el("span", "ac-dot");
statusBadge.appendChild(dot);
statusBadge.appendChild(document.createTextNode(frontmatter.statut));
acTop.appendChild(statusBadge);
}
// Category badge
if (frontmatter.catégorie || frontmatter.categorie) {
const cat = frontmatter.catégorie || frontmatter.categorie;
const catBadge = el("span", "ac-badge blue");
catBadge.textContent = cat;
acTop.appendChild(catBadge);
}
// Publish badge
if (frontmatter.publish) {
const pubBadge = el("span", "ac-badge purple");
pubBadge.textContent = "publié";
acTop.appendChild(pubBadge);
}
// Favoris badge
if (frontmatter.favoris) {
const favBadge = el("span", "ac-badge purple");
favBadge.textContent = "favori";
acTop.appendChild(favBadge);
}
// ZONE 2: Body 2 columns
const leftCol = el("div", "ac-col");
const row1 = el("div", "ac-row");
row1.innerHTML = '<span class="ac-k">auteur</span><span class="ac-v">' + (frontmatter.auteur || '—') + '</span>';
leftCol.appendChild(row1);
const row2 = el("div", "ac-row");
row2.innerHTML = '<span class="ac-k">catégorie</span><span class="ac-v">' + (frontmatter.catégorie || frontmatter.categorie || '—') + '</span>';
leftCol.appendChild(row2);
const row3 = el("div", "ac-row");
row3.innerHTML = '<span class="ac-k">statut</span><span class="ac-v">' + (frontmatter.statut || '—') + '</span>';
leftCol.appendChild(row3);
const row4 = el("div", "ac-row");
const aliases = frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(', ') : '[]';
row4.innerHTML = '<span class="ac-k">aliases</span><span class="ac-v muted">' + aliases + '</span>';
leftCol.appendChild(row4);
const rightCol = el("div", "ac-col");
const row5 = el("div", "ac-row");
row5.innerHTML = '<span class="ac-k">creation_date</span><span class="ac-v mono">' + formatDate(frontmatter.creation_date) + '</span>';
rightCol.appendChild(row5);
const row6 = el("div", "ac-row");
row6.innerHTML = '<span class="ac-k">modification_date</span><span class="ac-v mono">' + formatDate(frontmatter.modification_date) + '</span>';
rightCol.appendChild(row6);
const row7 = el("div", "ac-row");
row7.innerHTML = '<span class="ac-k">publish</span><span class="ac-v">' + String(frontmatter.publish || false) + '</span>';
rightCol.appendChild(row7);
const row8 = el("div", "ac-row");
row8.innerHTML = '<span class="ac-k">favoris</span><span class="ac-v">' + String(frontmatter.favoris || false) + '</span>';
rightCol.appendChild(row8);
const acBody = el("div", "ac-body");
acBody.appendChild(leftCol);
acBody.appendChild(rightCol);
// ZONE 3: Tags row
const acTagsRow = el("div", "ac-tags-row");
const tagsLabel = el("span", "ac-tags-k");
tagsLabel.textContent = "tags";
acTagsRow.appendChild(tagsLabel);
const tagsWrap = el("div", "ac-tags-wrap");
if (frontmatter.tags && frontmatter.tags.length > 0) {
frontmatter.tags.forEach(tag => {
const tagSpan = el("span", "ac-tag");
tagSpan.textContent = tag;
tagsWrap.appendChild(tagSpan);
});
}
acTagsRow.appendChild(tagsWrap);
// ZONE 4: Flags row
const acFlagsRow = el("div", "ac-flags-row");
const flagsLabel = el("span", "ac-flags-k");
flagsLabel.textContent = "flags";
acFlagsRow.appendChild(flagsLabel);
booleanFlags.forEach(flag => {
const chipClass = flag.value ? "flag-chip on" : "flag-chip off";
const chip = el("span", chipClass);
const dot = el("span", "flag-dot");
chip.appendChild(dot);
chip.appendChild(document.createTextNode(flag.key));
acFlagsRow.appendChild(chip);
});
// Assemble the card
const acCard = el("div", "ac-card");
acCard.appendChild(acTop);
acCard.appendChild(acBody);
acCard.appendChild(acTagsRow);
acCard.appendChild(acFlagsRow);
// Toggle functionality
fmHeader.addEventListener("click", () => {
isOpen = !isOpen;
if (isOpen) {
acCard.style.display = "block";
chevron.classList.remove("closed");
chevron.classList.add("open");
} else {
acCard.style.display = "none";
chevron.classList.remove("open");
chevron.classList.add("closed");
}
if (window.lucide) window.lucide.createIcons();
});
// Hide card initially (collapsed by default)
acCard.style.display = "none";
// Wrap in section
const fmSection = el("div", "fm-section");
fmSection.appendChild(fmHeader);
fmSection.appendChild(acCard);
return fmSection;
}
initTheme();
window.addEventListener('storage', (event) => {
if (event.key === 'obsigate-theme' && event.newValue) {
applyTheme(event.newValue);
}
});
document.getElementById("editor-cancel").addEventListener("click", closeEditor);
document.getElementById("editor-save").addEventListener("click", saveFile);
initPopout();
</script>
</body>
</html>