From fd41175ad82e1b761794db7e731d5073580dcea1 Mon Sep 17 00:00:00 2001
From: bruno
Date: Sun, 26 Jul 2026 11:06:53 -0400
Subject: [PATCH] =?UTF-8?q?fix:=20revue=20compl=C3=A8te=20obsigate-desktop?=
=?UTF-8?q?=20=E2=80=94=206=20correctifs=20pour=20le=20prochain=20build?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. main.rs: .current_dir(exe_dir) pour que Python trouve backend/ (pas le cwd hérité)
2. build-windows.bat: dir bundles pointe vers target/x86_64-.../release (était target/release)
3. build-windows.bat: supprimer étape 3 inutile (frontend-static) — Tauri utilise ../frontend
4. capabilities/default.json: requis par Tauri v2 (permissions plugins)
5. tauri.conf.json + Cargo.toml: retirer updater (placeholder sans clé)
6. .gitignore: ajouter desktop/target/, python-embed/, frontend-static/
7. build-linux.sh: mêmes corrections (bundles path, simplification)
---
.gitignore | 6 +
desktop/Cargo.toml | 1 -
desktop/build-linux.sh | 30 +-
desktop/build-windows.bat | 40 +-
desktop/capabilities/default.json | 14 +
desktop/frontend-static/app.js | 8905 -----------------
desktop/frontend-static/editor-poc.html | 1998 ----
desktop/frontend-static/icons/README.md | 42 -
.../frontend-static/icons/icon-128x128.svg | 27 -
.../frontend-static/icons/icon-144x144.svg | 27 -
.../frontend-static/icons/icon-152x152.svg | 27 -
.../icons/icon-192x192-maskable.svg | 27 -
.../frontend-static/icons/icon-192x192.svg | 27 -
.../frontend-static/icons/icon-384x384.svg | 27 -
.../icons/icon-512x512-maskable.svg | 27 -
.../frontend-static/icons/icon-512x512.svg | 27 -
desktop/frontend-static/icons/icon-72x72.svg | 27 -
desktop/frontend-static/icons/icon-96x96.svg | 27 -
.../frontend-static/icons/search-96x96.svg | 18 -
desktop/frontend-static/index.html | 4827 ---------
desktop/frontend-static/js/ai.js | 457 -
desktop/frontend-static/js/app.js | 107 -
desktop/frontend-static/js/auth.js | 579 --
desktop/frontend-static/js/autocomplete.js | 350 -
desktop/frontend-static/js/backup-manager.js | 324 -
desktop/frontend-static/js/backups.js | 501 -
desktop/frontend-static/js/config.js | 1570 ---
desktop/frontend-static/js/dashboard.js | 484 -
desktop/frontend-static/js/dragdrop.js | 329 -
desktop/frontend-static/js/graph.js | 949 --
desktop/frontend-static/js/i18n.js | 193 -
desktop/frontend-static/js/legacy.js | 417 -
desktop/frontend-static/js/mermaid-viewer.js | 845 --
desktop/frontend-static/js/mobile-toolbar.js | 149 -
desktop/frontend-static/js/offline-db.js | 377 -
desktop/frontend-static/js/offline.js | 209 -
desktop/frontend-static/js/package.json | 1 -
desktop/frontend-static/js/palette.js | 590 --
desktop/frontend-static/js/pane-manager.js | 263 -
desktop/frontend-static/js/search.js | 1154 ---
desktop/frontend-static/js/sidebar.js | 1124 ---
desktop/frontend-static/js/sidebar_raw.js | 1087 --
desktop/frontend-static/js/state.js | 59 -
desktop/frontend-static/js/sync.js | 643 --
desktop/frontend-static/js/themes.js | 566 --
desktop/frontend-static/js/ui.js | 2351 -----
desktop/frontend-static/js/utils.js | 679 --
desktop/frontend-static/js/vaulthome.js | 384 -
desktop/frontend-static/js/viewer.js | 1550 ---
desktop/frontend-static/locales/en.json | 1499 ---
desktop/frontend-static/locales/fr.json | 1499 ---
desktop/frontend-static/manifest.json | 89 -
desktop/frontend-static/popout.html | 1635 ---
desktop/frontend-static/style.css | 8573 ----------------
desktop/frontend-static/sw.js | 197 -
desktop/python-embed/LICENSE.txt | 702 --
desktop/python-embed/_asyncio.pyd | Bin 65304 -> 0 bytes
desktop/python-embed/_bz2.pyd | Bin 84760 -> 0 bytes
desktop/python-embed/_ctypes.pyd | Bin 124696 -> 0 bytes
desktop/python-embed/_decimal.pyd | Bin 255768 -> 0 bytes
desktop/python-embed/_elementtree.pyd | Bin 128792 -> 0 bytes
desktop/python-embed/_hashlib.pyd | Bin 65304 -> 0 bytes
desktop/python-embed/_lzma.pyd | Bin 159512 -> 0 bytes
desktop/python-embed/_msi.pyd | Bin 44824 -> 0 bytes
desktop/python-embed/_multiprocessing.pyd | Bin 34584 -> 0 bytes
desktop/python-embed/_overlapped.pyd | Bin 51480 -> 0 bytes
desktop/python-embed/_queue.pyd | Bin 32024 -> 0 bytes
desktop/python-embed/_socket.pyd | Bin 79640 -> 0 bytes
desktop/python-embed/_sqlite3.pyd | Bin 120600 -> 0 bytes
desktop/python-embed/_ssl.pyd | Bin 178456 -> 0 bytes
desktop/python-embed/_uuid.pyd | Bin 25368 -> 0 bytes
desktop/python-embed/_zoneinfo.pyd | Bin 45848 -> 0 bytes
desktop/python-embed/libcrypto-3.dll | Bin 5191960 -> 0 bytes
desktop/python-embed/libffi-8.dll | Bin 39696 -> 0 bytes
desktop/python-embed/libssl-3.dll | Bin 787224 -> 0 bytes
desktop/python-embed/pyexpat.pyd | Bin 199448 -> 0 bytes
desktop/python-embed/python.cat | Bin 643161 -> 0 bytes
desktop/python-embed/python.exe | Bin 103192 -> 0 bytes
desktop/python-embed/python3.dll | Bin 67352 -> 0 bytes
desktop/python-embed/python311._pth | 2 -
desktop/python-embed/python311.dll | Bin 5800216 -> 0 bytes
desktop/python-embed/python311.zip | Bin 4285636 -> 0 bytes
desktop/python-embed/pythonw.exe | Bin 101656 -> 0 bytes
desktop/python-embed/select.pyd | Bin 30488 -> 0 bytes
desktop/python-embed/sidecar.py | 72 -
desktop/python-embed/sqlite3.dll | Bin 1540888 -> 0 bytes
desktop/python-embed/unicodedata.pyd | Bin 1140504 -> 0 bytes
desktop/python-embed/vcruntime140.dll | Bin 119192 -> 0 bytes
desktop/python-embed/vcruntime140_1.dll | Bin 49528 -> 0 bytes
desktop/python-embed/winsound.pyd | Bin 31512 -> 0 bytes
desktop/src/main.rs | 3 +-
desktop/tauri.conf.json | 6 -
92 files changed, 46 insertions(+), 48673 deletions(-)
create mode 100644 desktop/capabilities/default.json
delete mode 100644 desktop/frontend-static/app.js
delete mode 100644 desktop/frontend-static/editor-poc.html
delete mode 100644 desktop/frontend-static/icons/README.md
delete mode 100644 desktop/frontend-static/icons/icon-128x128.svg
delete mode 100644 desktop/frontend-static/icons/icon-144x144.svg
delete mode 100644 desktop/frontend-static/icons/icon-152x152.svg
delete mode 100644 desktop/frontend-static/icons/icon-192x192-maskable.svg
delete mode 100644 desktop/frontend-static/icons/icon-192x192.svg
delete mode 100644 desktop/frontend-static/icons/icon-384x384.svg
delete mode 100644 desktop/frontend-static/icons/icon-512x512-maskable.svg
delete mode 100644 desktop/frontend-static/icons/icon-512x512.svg
delete mode 100644 desktop/frontend-static/icons/icon-72x72.svg
delete mode 100644 desktop/frontend-static/icons/icon-96x96.svg
delete mode 100644 desktop/frontend-static/icons/search-96x96.svg
delete mode 100644 desktop/frontend-static/index.html
delete mode 100644 desktop/frontend-static/js/ai.js
delete mode 100644 desktop/frontend-static/js/app.js
delete mode 100644 desktop/frontend-static/js/auth.js
delete mode 100644 desktop/frontend-static/js/autocomplete.js
delete mode 100644 desktop/frontend-static/js/backup-manager.js
delete mode 100644 desktop/frontend-static/js/backups.js
delete mode 100644 desktop/frontend-static/js/config.js
delete mode 100644 desktop/frontend-static/js/dashboard.js
delete mode 100644 desktop/frontend-static/js/dragdrop.js
delete mode 100644 desktop/frontend-static/js/graph.js
delete mode 100644 desktop/frontend-static/js/i18n.js
delete mode 100644 desktop/frontend-static/js/legacy.js
delete mode 100644 desktop/frontend-static/js/mermaid-viewer.js
delete mode 100644 desktop/frontend-static/js/mobile-toolbar.js
delete mode 100644 desktop/frontend-static/js/offline-db.js
delete mode 100644 desktop/frontend-static/js/offline.js
delete mode 100644 desktop/frontend-static/js/package.json
delete mode 100644 desktop/frontend-static/js/palette.js
delete mode 100644 desktop/frontend-static/js/pane-manager.js
delete mode 100644 desktop/frontend-static/js/search.js
delete mode 100644 desktop/frontend-static/js/sidebar.js
delete mode 100644 desktop/frontend-static/js/sidebar_raw.js
delete mode 100644 desktop/frontend-static/js/state.js
delete mode 100644 desktop/frontend-static/js/sync.js
delete mode 100644 desktop/frontend-static/js/themes.js
delete mode 100644 desktop/frontend-static/js/ui.js
delete mode 100644 desktop/frontend-static/js/utils.js
delete mode 100644 desktop/frontend-static/js/vaulthome.js
delete mode 100644 desktop/frontend-static/js/viewer.js
delete mode 100644 desktop/frontend-static/locales/en.json
delete mode 100644 desktop/frontend-static/locales/fr.json
delete mode 100644 desktop/frontend-static/manifest.json
delete mode 100644 desktop/frontend-static/popout.html
delete mode 100644 desktop/frontend-static/style.css
delete mode 100644 desktop/frontend-static/sw.js
delete mode 100644 desktop/python-embed/LICENSE.txt
delete mode 100644 desktop/python-embed/_asyncio.pyd
delete mode 100644 desktop/python-embed/_bz2.pyd
delete mode 100644 desktop/python-embed/_ctypes.pyd
delete mode 100644 desktop/python-embed/_decimal.pyd
delete mode 100644 desktop/python-embed/_elementtree.pyd
delete mode 100644 desktop/python-embed/_hashlib.pyd
delete mode 100644 desktop/python-embed/_lzma.pyd
delete mode 100644 desktop/python-embed/_msi.pyd
delete mode 100644 desktop/python-embed/_multiprocessing.pyd
delete mode 100644 desktop/python-embed/_overlapped.pyd
delete mode 100644 desktop/python-embed/_queue.pyd
delete mode 100644 desktop/python-embed/_socket.pyd
delete mode 100644 desktop/python-embed/_sqlite3.pyd
delete mode 100644 desktop/python-embed/_ssl.pyd
delete mode 100644 desktop/python-embed/_uuid.pyd
delete mode 100644 desktop/python-embed/_zoneinfo.pyd
delete mode 100644 desktop/python-embed/libcrypto-3.dll
delete mode 100644 desktop/python-embed/libffi-8.dll
delete mode 100644 desktop/python-embed/libssl-3.dll
delete mode 100644 desktop/python-embed/pyexpat.pyd
delete mode 100644 desktop/python-embed/python.cat
delete mode 100644 desktop/python-embed/python.exe
delete mode 100644 desktop/python-embed/python3.dll
delete mode 100644 desktop/python-embed/python311._pth
delete mode 100644 desktop/python-embed/python311.dll
delete mode 100644 desktop/python-embed/python311.zip
delete mode 100644 desktop/python-embed/pythonw.exe
delete mode 100644 desktop/python-embed/select.pyd
delete mode 100644 desktop/python-embed/sidecar.py
delete mode 100644 desktop/python-embed/sqlite3.dll
delete mode 100644 desktop/python-embed/unicodedata.pyd
delete mode 100644 desktop/python-embed/vcruntime140.dll
delete mode 100644 desktop/python-embed/vcruntime140_1.dll
delete mode 100644 desktop/python-embed/winsound.pyd
diff --git a/.gitignore b/.gitignore
index 253adc3..4775b35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,9 @@ odysseus/
node_modules/
test-results/
playwright-report/
+
+# Desktop build artifacts
+desktop/target/
+desktop/python-embed/
+desktop/frontend-static/
+
diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml
index 6eb7fa6..98a1b53 100644
--- a/desktop/Cargo.toml
+++ b/desktop/Cargo.toml
@@ -15,7 +15,6 @@ tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-fs = "2"
tauri-plugin-process = "2"
-tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
diff --git a/desktop/build-linux.sh b/desktop/build-linux.sh
index 3871116..1ca9dea 100755
--- a/desktop/build-linux.sh
+++ b/desktop/build-linux.sh
@@ -11,7 +11,7 @@ echo "========================================"
echo ""
# ── 1. Vérifier les prérequis ─────────────────────────────────
-echo "[1/6] Verifying prerequisites..."
+echo "[1/4] Verifying prerequisites..."
command -v cargo >/dev/null 2>&1 || { echo "❌ Rust/Cargo not found. Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"; exit 1; }
command -v tauri >/dev/null 2>&1 || cargo install tauri-cli
@@ -22,32 +22,22 @@ fi
echo "✅ Dependencies OK"
# ── 2. Préparer Python 3.11 ────────────────────────────────────
-echo "[2/6] Preparing Python environment..."
+echo "[2/4] Preparing Python environment..."
python3 -m venv python-embed/venv
source python-embed/venv/bin/activate
pip install --upgrade pip -q
pip install -r ../backend/requirements.txt -q
echo "✅ Python environment ready"
-# ── 3. Copier le frontend ─────────────────────────────────────
-echo "[3/6] Copying frontend..."
-rm -rf frontend-static
-cp -r ../frontend frontend-static
-echo "✅ Frontend copied"
-
-# ── 4. Icônes ──────────────────────────────────────────────────
-echo "[4/6] Generating icons..."
-# Placeholder — tauri icon generator
-echo "✅ Icons OK"
-
-# ── 5. Build Tauri ────────────────────────────────────────────
-echo "[5/6] Building Tauri application..."
+# ── 3. Build Tauri ────────────────────────────────────────────
+echo "[3/4] Building Tauri application..."
cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
echo "✅ Build successful"
-# ── 6. Résultats ───────────────────────────────────────────────
-echo "[6/6] Results:"
-ls -lh target/release/bundle/deb/*.deb 2>/dev/null
-ls -lh target/release/bundle/appimage/*.AppImage 2>/dev/null
+# ── 4. Résultats ───────────────────────────────────────────────
+OUTDIR="target/x86_64-unknown-linux-gnu/release"
+echo "[4/4] Results:"
+ls -lh "$OUTDIR/bundle/deb/"*.deb 2>/dev/null
+ls -lh "$OUTDIR/bundle/appimage/"*.AppImage 2>/dev/null
echo ""
-echo "✅ Done! Check target/release/bundle/"
+echo "✅ Done! Check $OUTDIR/bundle/"
diff --git a/desktop/build-windows.bat b/desktop/build-windows.bat
index 1a61675..ed34ecb 100644
--- a/desktop/build-windows.bat
+++ b/desktop/build-windows.bat
@@ -11,41 +11,30 @@ echo ========================================
echo.
REM ── 1. Vérifier les prérequis ─────────────────────────────────
-echo [1/6] Verifying prerequisites...
+echo [1/5] Verifying prerequisites...
where cargo >nul 2>&1 || (echo ❌ Rust/Cargo not found. Install from https://rustup.rs & exit /b 1)
where tauri >nul 2>&1 || cargo install tauri-cli
echo ✅ Rust + Tauri OK
REM ── 2. Préparer Python 3.11 embed ─────────────────────────────
-echo [2/6] Preparing Python embed...
+echo [2/5] Preparing Python embed...
if not exist "python-embed\python.exe" (
echo 📥 Downloading Python 3.11 embed...
if exist "python-embed" rmdir /s /q "python-embed"
curl -L -o python-embed.zip "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip"
powershell Expand-Archive -Path python-embed.zip -DestinationPath python-embed\
del python-embed.zip
-
+
REM Enable pip in embedded Python
echo import site >> python-embed\python311._pth
-
+
REM Install dependencies
python-embed\python.exe -m pip install --no-cache-dir -r ..\backend\requirements.txt
)
echo ✅ Python embed ready
-REM ── 3. Copier le frontend ─────────────────────────────────────
-echo [3/6] Copying frontend...
-if exist "frontend-static" rmdir /s /q "frontend-static"
-xcopy /E /I /Q "..\frontend" "frontend-static"
-echo ✅ Frontend copied
-
-REM ── 4. Mettre à jour les icônes ───────────────────────────────
-echo [4/6] Generating icons...
-REM Placeholder — will use tauri icon generator when icons are ready
-echo ✅ Icons OK (using defaults)
-
-REM ── 5. Build Tauri ────────────────────────────────────────────
-echo [5/6] Building Tauri application...
+REM ── 3. Build Tauri ────────────────────────────────────────────
+echo [3/5] Building Tauri application...
cargo tauri build --target x86_64-pc-windows-msvc --bundles msi,nsis
if errorlevel 1 (
echo ❌ Build failed!
@@ -53,20 +42,19 @@ if errorlevel 1 (
)
echo ✅ Build successful
-REM ── 5b. Copier python-embed à côté de l'exécutable pour le dev ─────
-echo [5b/6] Copying python-embed to output dir...
+REM ── 4. Copier le runtime à côté de l'exécutable ───────────────
+echo [4/5] Copying runtime to output dir...
set "OUTDIR=target\x86_64-pc-windows-msvc\release"
if exist "%OUTDIR%\python-embed" rmdir /s /q "%OUTDIR%\python-embed"
+if exist "%OUTDIR%\backend" rmdir /s /q "%OUTDIR%\backend"
xcopy /E /I /Q "python-embed" "%OUTDIR%\python-embed"
-REM Copier aussi le backend Python
-if not exist "%OUTDIR%\backend" mkdir "%OUTDIR%\backend"
xcopy /E /I /Q "..\backend" "%OUTDIR%\backend"
echo ✅ Runtime copied
-REM ── 6. Résultats ───────────────────────────────────────────────────
-echo [6/6] Results:
-dir /b target\release\bundle\msi\*.msi 2>nul
-dir /b target\release\bundle\nsis\*.exe 2>nul
+REM ── 5. Résultats ───────────────────────────────────────────────
+echo [5/5] Results:
+dir /b "%OUTDIR%\bundle\msi\*.msi" 2>nul
+dir /b "%OUTDIR%\bundle\nsis\*.exe" 2>nul
echo.
-echo ✅ Done! Check target\release\bundle\
+echo ✅ Done! Check %OUTDIR%\bundle\
endlocal
diff --git a/desktop/capabilities/default.json b/desktop/capabilities/default.json
new file mode 100644
index 0000000..34c5404
--- /dev/null
+++ b/desktop/capabilities/default.json
@@ -0,0 +1,14 @@
+{
+ "identifier": "default",
+ "description": "Default capabilities for ObsiGate Desktop",
+ "windows": ["main"],
+ "permissions": [
+ "core:default",
+ "shell:allow-open",
+ "shell:allow-execute",
+ "dialog:default",
+ "notification:default",
+ "fs:default",
+ "process:default"
+ ]
+}
diff --git a/desktop/frontend-static/app.js b/desktop/frontend-static/app.js
deleted file mode 100644
index 6a51793..0000000
--- a/desktop/frontend-static/app.js
+++ /dev/null
@@ -1,8905 +0,0 @@
-/* ObsiGate — Vanilla JS SPA */
-
-(function () {
- "use strict";
-
- const APP_VERSION = "1.5.0";
-
- // ---------------------------------------------------------------------------
- // State
- // ---------------------------------------------------------------------------
- let currentVault = null;
- let currentPath = null;
- let searchTimeout = null;
- let searchAbortController = null;
- let showingSource = false;
- let cachedRawSource = null;
- let allVaults = [];
- let selectedContextVault = "all";
- let selectedTags = [];
- let editorView = null;
- let editorVault = null;
- let editorPath = null;
- let fallbackEditorEl = null;
- let sidebarFilterCaseSensitive = false;
- let searchCaseSensitive = false;
- let searchWholeWord = false;
- let searchRegex = false;
- let searchFilterVisible = false;
- let _iconDebounceTimer = null;
- let activeSidebarTab = "vaults";
- let filterDebounce = null;
-
- // Vault settings cache for hideHiddenFiles
- let vaultSettings = {};
-
- // Advanced search state
- let advancedSearchOffset = 0;
- let advancedSearchTotal = 0;
- let advancedSearchSort = "relevance";
- let advancedSearchLastQuery = "";
- let suggestAbortController = null;
- let dropdownActiveIndex = -1;
- let dropdownItems = [];
- let currentSearchId = 0;
-
- // Advanced search constants
- const SEARCH_HISTORY_KEY = "obsigate_search_history";
- const MAX_HISTORY_ENTRIES = 50;
- const SUGGEST_DEBOUNCE_MS = 150;
- const ADVANCED_SEARCH_LIMIT = 50;
- const MIN_SEARCH_LENGTH = 2;
- const SEARCH_TIMEOUT_MS = 30000;
-
- // Outline/TOC state
- let outlineObserver = null;
- let activeHeadingId = null;
- let headingsCache = [];
- let rightSidebarVisible = false;
- let rightSidebarWidth = 280;
-
- // ---------------------------------------------------------------------------
- // File extension → Lucide icon mapping
- // ---------------------------------------------------------------------------
- const EXT_ICONS = {
- // Text files
- ".md": "file-text",
- ".txt": "file-text",
- ".log": "file-text",
- ".readme": "file-text",
- ".rst": "file-text",
- ".adoc": "file-text",
-
- // Web development
- ".html": "file-code",
- ".htm": "file-code",
- ".css": "file-code",
- ".scss": "file-code",
- ".sass": "file-code",
- ".less": "file-code",
- ".js": "file-code",
- ".jsx": "file-code",
- ".ts": "file-code",
- ".tsx": "file-code",
- ".vue": "file-code",
- ".svelte": "file-code",
-
- // Programming languages
- ".py": "file-code",
- ".java": "file-code",
- ".c": "file-code",
- ".cpp": "file-code",
- ".cc": "file-code",
- ".cxx": "file-code",
- ".h": "file-code",
- ".hpp": "file-code",
- ".cs": "file-code",
- ".go": "file-code",
- ".rs": "file-code",
- ".rb": "file-code",
- ".php": "file-code",
- ".swift": "file-code",
- ".kt": "file-code",
- ".scala": "file-code",
- ".r": "file-code",
- ".m": "file-code",
- ".pl": "file-code",
- ".lua": "file-code",
- ".dart": "file-code",
- ".nim": "file-code",
- ".zig": "file-code",
- ".odin": "file-code",
- ".v": "file-code",
- ".cr": "file-code",
- ".ex": "file-code",
- ".exs": "file-code",
- ".elm": "file-code",
- ".purs": "file-code",
- ".hs": "file-code",
- ".ml": "file-code",
- ".ocaml": "file-code",
- ".fs": "file-code",
- ".fsx": "file-code",
- ".vb": "file-code",
- ".pas": "file-code",
- ".pp": "file-code",
- ".inc": "file-code",
-
- // Data formats
- ".json": "file-json",
- ".yaml": "file-cog",
- ".yml": "file-cog",
- ".toml": "file-cog",
- ".xml": "file-code",
- ".csv": "table",
- ".tsv": "table",
- ".sql": "database",
- ".db": "database",
- ".sqlite": "database",
- ".sqlite3": "database",
- ".parquet": "database",
- ".avro": "database",
-
- // Configuration files
- ".ini": "file-cog",
- ".cfg": "file-cog",
- ".conf": "file-cog",
- ".env": "file-cog",
- ".dockerfile": "file-cog",
- ".gitignore": "file-cog",
- ".gitattributes": "file-cog",
- ".editorconfig": "file-cog",
- ".eslintrc": "file-cog",
- ".prettierrc": "file-cog",
- ".babelrc": "file-cog",
- ".tsconfig": "file-cog",
- "package.json": "file-cog",
- "package-lock.json": "file-cog",
- "yarn.lock": "file-cog",
- "composer.json": "file-cog",
- "requirements.txt": "file-cog",
- "pipfile": "file-cog",
- "gemfile": "file-cog",
- "cargo.toml": "file-cog",
- "go.mod": "file-cog",
- "go.sum": "file-cog",
- "pom.xml": "file-cog",
- "build.gradle": "file-cog",
- "cmakelists.txt": "file-cog",
- "makefile": "file-cog",
-
- // Shell scripts
- ".sh": "terminal",
- ".bash": "terminal",
- ".zsh": "terminal",
- ".fish": "terminal",
- ".bat": "terminal",
- ".cmd": "terminal",
- ".ps1": "terminal",
- ".psm1": "terminal",
- ".psd1": "terminal",
-
- // Document formats
- ".pdf": "file-text",
- ".doc": "file-text",
- ".docx": "file-text",
- ".rtf": "file-text",
- ".odt": "file-text",
- ".tex": "file-text",
- ".latex": "file-text",
-
- // Image files
- ".png": "file-image",
- ".jpg": "file-image",
- ".jpeg": "file-image",
- ".gif": "file-image",
- ".svg": "file-image",
- ".webp": "file-image",
- ".bmp": "file-image",
- ".ico": "file-image",
- ".tiff": "file-image",
- ".tif": "file-image",
-
- // Audio files
- ".mp3": "file-music",
- ".wav": "file-music",
- ".flac": "file-music",
- ".aac": "file-music",
- ".ogg": "file-music",
- ".m4a": "file-music",
- ".wma": "file-music",
-
- // Video files
- ".mp4": "play",
- ".avi": "play",
- ".mov": "play",
- ".wmv": "play",
- ".flv": "play",
- ".webm": "play",
- ".mkv": "play",
- ".m4v": "play",
- ".3gp": "play",
-
- // Archive files
- ".zip": "file-archive",
- ".rar": "file-archive",
- ".7z": "file-archive",
- ".tar": "file-archive",
- ".gz": "file-archive",
- ".tgz": "file-archive",
- ".bz2": "file-archive",
- ".xz": "file-archive",
- ".deb": "file-archive",
- ".rpm": "file-archive",
- ".dmg": "file-archive",
- ".pkg": "file-archive",
- ".msi": "file-archive",
- ".exe": "file-archive",
-
- // Font files
- ".ttf": "file-type",
- ".otf": "file-type",
- ".woff": "file-type",
- ".woff2": "file-type",
- ".eot": "file-type",
-
- // Other common files
- ".key": "file-cog",
- ".pem": "file-cog",
- ".crt": "file-cog",
- ".cert": "file-cog",
- ".p12": "file-cog",
- ".pfx": "file-cog",
- ".lock": "file-cog",
- ".tmp": "file",
- ".bak": "file",
- ".old": "file",
- ".orig": "file",
- ".save": "file",
- };
-
- function getFileIcon(name) {
- const ext = "." + name.split(".").pop().toLowerCase();
- return EXT_ICONS[ext] || "file";
- }
-
- // ---------------------------------------------------------------------------
- // Search History Service (localStorage, LIFO, max 50, dedup)
- // ---------------------------------------------------------------------------
- const SearchHistory = {
- _load() {
- try {
- const raw = localStorage.getItem(SEARCH_HISTORY_KEY);
- return raw ? JSON.parse(raw) : [];
- } catch {
- return [];
- }
- },
- _save(entries) {
- try {
- localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries));
- } catch {}
- },
- getAll() {
- return this._load();
- },
- add(query) {
- if (!query || !query.trim()) return;
- const q = query.trim();
- let entries = this._load().filter((e) => e !== q);
- entries.unshift(q);
- if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES);
- this._save(entries);
- },
- remove(query) {
- const entries = this._load().filter((e) => e !== query);
- this._save(entries);
- },
- clear() {
- this._save([]);
- },
- filter(prefix) {
- if (!prefix) return this.getAll().slice(0, 8);
- const lp = prefix.toLowerCase();
- return this._load()
- .filter((e) => e.toLowerCase().includes(lp))
- .slice(0, 8);
- },
- };
-
- // ---------------------------------------------------------------------------
- // Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:)
- // ---------------------------------------------------------------------------
- const QueryParser = {
- parse(raw) {
- const result = { tags: [], vault: null, title: null, path: null, ext: null, freeText: "" };
- if (!raw) return result;
- const tokens = this._tokenize(raw);
- const freeTokens = [];
- for (const tok of tokens) {
- const lower = tok.toLowerCase();
- if (lower.startsWith("tag:")) {
- const v = tok.slice(4).replace(/"/g, "").trim().replace(/^#/, "");
- if (v) result.tags.push(v);
- } else if (lower.startsWith("#") && tok.length > 1) {
- result.tags.push(tok.slice(1));
- } else if (lower.startsWith("vault:")) {
- result.vault = tok.slice(6).replace(/"/g, "").trim();
- } else if (lower.startsWith("title:")) {
- result.title = tok.slice(6).replace(/"/g, "").trim();
- } else if (lower.startsWith("path:")) {
- result.path = tok.slice(5).replace(/"/g, "").trim();
- } else if (lower.startsWith("ext:")) {
- result.ext = tok.slice(4).replace(/"/g, "").trim().replace(/^\./, "").toLowerCase();
- } else {
- freeTokens.push(tok);
- }
- }
- result.freeText = freeTokens.join(" ");
- return result;
- },
- _tokenize(raw) {
- const tokens = [];
- let i = 0;
- const n = raw.length;
- while (i < n) {
- while (i < n && raw[i] === " ") i++;
- if (i >= n) break;
- if (raw[i] !== '"') {
- let j = i;
- while (j < n && raw[j] !== " ") {
- if (raw[j] === '"') {
- j++;
- while (j < n && raw[j] !== '"') j++;
- if (j < n) j++;
- } else j++;
- }
- tokens.push(raw.slice(i, j).replace(/"/g, ""));
- i = j;
- } else {
- i++;
- let j = i;
- while (j < n && raw[j] !== '"') j++;
- tokens.push(raw.slice(i, j));
- i = j + 1;
- }
- }
- return tokens;
- },
- /** Detect the current operator context at cursor for autocomplete */
- getContext(raw, cursorPos) {
- const before = raw.slice(0, cursorPos);
- // Check if we're typing a tag: or # value
- const tagMatch = before.match(/(?:tag:|#)([\w-]*)$/i);
- if (tagMatch) return { type: "tag", prefix: tagMatch[1] };
- // Check if typing title:
- const titleMatch = before.match(/title:([\w-]*)$/i);
- if (titleMatch) return { type: "title", prefix: titleMatch[1] };
- // Default: free text
- const words = before.trim().split(/\s+/);
- const lastWord = words[words.length - 1] || "";
- return { type: "text", prefix: lastWord };
- },
- };
-
- // ---------------------------------------------------------------------------
- // Autocomplete Dropdown Controller
- // ---------------------------------------------------------------------------
- const AutocompleteDropdown = {
- _dropdown: null,
- _historySection: null,
- _titlesSection: null,
- _tagsSection: null,
- _historyList: null,
- _titlesList: null,
- _tagsList: null,
- _emptyEl: null,
- _suggestTimer: null,
-
- init() {
- this._dropdown = document.getElementById("search-dropdown");
- this._historySection = document.getElementById("search-dropdown-history");
- this._titlesSection = document.getElementById("search-dropdown-titles");
- this._tagsSection = document.getElementById("search-dropdown-tags");
- this._historyList = document.getElementById("search-dropdown-history-list");
- this._titlesList = document.getElementById("search-dropdown-titles-list");
- this._tagsList = document.getElementById("search-dropdown-tags-list");
- this._emptyEl = document.getElementById("search-dropdown-empty");
-
- // Clear history button
- const clearBtn = document.getElementById("search-dropdown-clear-history");
- if (clearBtn) {
- clearBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- SearchHistory.clear();
- this.hide();
- });
- }
-
- // Close dropdown on outside click
- document.addEventListener("click", (e) => {
- if (this._dropdown && !this._dropdown.contains(e.target) && e.target.id !== "search-input") {
- this.hide();
- }
- });
- },
-
- show() {
- if (this._dropdown) this._dropdown.hidden = false;
- },
-
- hide() {
- if (this._dropdown) this._dropdown.hidden = true;
- dropdownActiveIndex = -1;
- dropdownItems = [];
- },
-
- isVisible() {
- return this._dropdown && !this._dropdown.hidden;
- },
-
- /** Populate and show the dropdown with history, title suggestions, and tag suggestions */
- async populate(inputValue, cursorPos) {
- if (this._suppressNext) { this._suppressNext = false; return; }
- // Cancel previous suggestion request
- if (suggestAbortController) {
- suggestAbortController.abort();
- suggestAbortController = null;
- }
-
- const ctx = QueryParser.getContext(inputValue, cursorPos);
- const vault = document.getElementById("vault-filter").value;
-
- // History — always show filtered history
- const historyItems = SearchHistory.filter(inputValue).slice(0, 5);
- this._renderHistory(historyItems, inputValue);
-
- // Title and tag suggestions from API (debounced) — always fetch both
- clearTimeout(this._suggestTimer);
- const prefix = ctx.prefix;
- if (prefix && prefix.length >= 2) {
- // Only show placeholder if lists are empty (avoid flashing on fast typing)
- const hasTitles = this._titlesList.children.length > 0 && !this._titlesList.querySelector(".search-dropdown__item--loading");
- const hasTags = this._tagsList.children.length > 0 && !this._tagsList.querySelector(".search-dropdown__item--loading");
- if (!hasTitles) {
- this._titlesList.innerHTML = 'Recherche... ';
- }
- if (!hasTags) {
- this._tagsList.innerHTML = 'Recherche... ';
- }
- this._titlesSection.hidden = false;
- this._tagsSection.hidden = false;
- this.show();
- this._suggestTimer = setTimeout(() => this._fetchSuggestions(prefix, vault), 150);
- } else {
- this._renderTitles([], "");
- this._renderTags([], "");
- this._titlesSection.hidden = true;
- this._tagsSection.hidden = true;
- }
-
- // Show/hide sections
- this._historySection.hidden = historyItems.length === 0;
- const hasContent = historyItems.length > 0;
- if (hasContent || (prefix && prefix.length >= 2)) {
- this.show();
- } else {
- this.hide();
- }
-
- this._collectItems();
- },
-
- async _fetchSuggestions(prefix, vault) {
- suggestAbortController = new AbortController();
- // Fetch titles
- try {
- const titlesRes = await api(`/api/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: suggestAbortController.signal });
- this._renderTitles(titlesRes.suggestions || [], prefix);
- this._titlesSection.hidden = !(titlesRes.suggestions || []).length;
- if (titlesRes.suggestions?.length) this.show();
- } catch (err) {
- if (err.name === "AbortError") return;
- this._titlesSection.hidden = true;
- }
- // Fetch tags — keep section always visible to confirm it works
- try {
- const tagsRes = await api(`/api/tags/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: suggestAbortController.signal });
- const items = tagsRes.suggestions || [];
- if (items.length > 0) {
- this._renderTags(items, prefix);
- } else {
- this._tagsList.innerHTML = 'Aucun tag ';
- }
- this._tagsSection.hidden = false;
- this.show();
- } catch (err) {
- if (err.name === "AbortError") return;
- this._tagsList.innerHTML = 'Erreur chargement ';
- this._tagsSection.hidden = false;
- }
- this._collectItems();
- },
-
- _renderHistory(items, query) {
- this._historyList.innerHTML = "";
- items.forEach((entry) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--history", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- textEl.textContent = entry;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- input.value = entry;
- this._suppressNext = true;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- this.hide();
- _triggerAdvancedSearch(entry);
- });
- this._historyList.appendChild(li);
- });
- },
-
- _renderTitles(items, prefix) {
- this._titlesList.innerHTML = "";
- items.forEach((item) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--title", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- if (prefix) {
- this._highlightText(textEl, item.title, prefix);
- } else {
- textEl.textContent = item.title;
- }
- const metaEl = el("span", { class: "search-dropdown__meta" });
- metaEl.textContent = item.vault;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.appendChild(metaEl);
- li.addEventListener("click", () => {
- this.hide();
- TabManager.openPreview(item.vault, item.path);
- });
- this._titlesList.appendChild(li);
- });
- },
-
- _renderTags(items, prefix) {
- this._tagsList.innerHTML = "";
- items.forEach((item) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--tag", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- if (prefix) {
- this._highlightText(textEl, item.tag, prefix);
- } else {
- textEl.textContent = item.tag;
- }
- const badge = el("span", { class: "search-dropdown__badge" });
- badge.textContent = item.count;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.appendChild(badge);
- li.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- const current = input.value;
- const cursorPos = input.selectionStart;
- const ctx = QueryParser.getContext(current, cursorPos);
- if (ctx.type === "tag") {
- // Replace the partial tag prefix
- const before = current.slice(0, cursorPos - ctx.prefix.length);
- input.value = before + item.tag + " ";
- } else {
- // Replace the last word with tag: operator
- const words = current.trim().split(/\s+/);
- if (words.length > 0 && ctx.prefix && ctx.prefix.length > 0) {
- words[words.length - 1] = ""; // remove last partial word
- }
- const base = words.filter(w => w).join(" ");
- input.value = (base ? base + " " : "") + "tag:" + item.tag + " ";
- }
- this._suppressNext = true;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- this.hide();
- input.focus();
- _triggerAdvancedSearch(input.value);
- });
- this._tagsList.appendChild(li);
- });
- },
-
- _highlightText(container, text, query) {
- const lower = text.toLowerCase();
- const needle = query.toLowerCase();
- const pos = lower.indexOf(needle);
- if (pos === -1) {
- container.textContent = text;
- return;
- }
- container.appendChild(document.createTextNode(text.slice(0, pos)));
- const markEl = el("mark", {}, [document.createTextNode(text.slice(pos, pos + query.length))]);
- container.appendChild(markEl);
- container.appendChild(document.createTextNode(text.slice(pos + query.length)));
- },
-
- _collectItems() {
- dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item"));
- dropdownActiveIndex = -1;
- dropdownItems.forEach((item) => item.classList.remove("active"));
- },
-
- navigateDown() {
- if (!this.isVisible() || dropdownItems.length === 0) return;
- if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
- dropdownActiveIndex = (dropdownActiveIndex + 1) % dropdownItems.length;
- dropdownItems[dropdownActiveIndex].classList.add("active");
- dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
- },
-
- navigateUp() {
- if (!this.isVisible() || dropdownItems.length === 0) return;
- if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
- dropdownActiveIndex = dropdownActiveIndex <= 0 ? dropdownItems.length - 1 : dropdownActiveIndex - 1;
- dropdownItems[dropdownActiveIndex].classList.add("active");
- dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
- },
-
- selectActive() {
- if (dropdownActiveIndex >= 0 && dropdownActiveIndex < dropdownItems.length) {
- dropdownItems[dropdownActiveIndex].click();
- return true;
- }
- return false;
- },
- };
-
- // ---------------------------------------------------------------------------
- // Search Chips Controller — renders active filter chips from parsed query
- // ---------------------------------------------------------------------------
- const SearchChips = {
- _container: null,
- init() {
- this._container = document.getElementById("search-chips");
- },
- update(parsed) {
- if (!this._container) return;
- this._container.innerHTML = "";
- let hasChips = false;
- parsed.tags.forEach((tag) => {
- this._addChip("tag", `tag:${tag}`, tag);
- hasChips = true;
- });
- if (parsed.vault) {
- this._addChip("vault", `vault:${parsed.vault}`, parsed.vault);
- hasChips = true;
- }
- if (parsed.title) {
- this._addChip("title", `title:${parsed.title}`, parsed.title);
- hasChips = true;
- }
- if (parsed.path) {
- this._addChip("path", `path:${parsed.path}`, parsed.path);
- hasChips = true;
- }
- if (parsed.ext) {
- this._addChip("ext", `ext:${parsed.ext}`, parsed.ext);
- hasChips = true;
- }
- this._container.hidden = !hasChips;
- },
- clear() {
- if (!this._container) return;
- this._container.innerHTML = "";
- this._container.hidden = true;
- },
- _addChip(type, fullOperator, displayText) {
- const chip = el("span", { class: `search-chip search-chip--${type}` });
- const label = el("span", { class: "search-chip__label" });
- label.textContent = fullOperator;
- const removeBtn = el("button", { class: "search-chip__remove", title: "Retirer ce filtre", type: "button" });
- removeBtn.innerHTML = ' ';
- removeBtn.addEventListener("click", () => {
- // Remove this operator from the input
- const input = document.getElementById("search-input");
- const raw = input.value;
- // Remove the operator text from the query
- const escaped = fullOperator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- input.value = raw.replace(new RegExp("\\s*" + escaped + "\\s*", "i"), " ").trim();
- _triggerAdvancedSearch(input.value);
- });
- chip.appendChild(label);
- chip.appendChild(removeBtn);
- this._container.appendChild(chip);
- safeCreateIcons();
- },
- };
-
- // ---------------------------------------------------------------------------
- // Helper: trigger advanced search from input value
- // ---------------------------------------------------------------------------
- function _triggerAdvancedSearch(rawQuery) {
- const q = (rawQuery || "").trim();
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
- advancedSearchOffset = 0;
- if (q.length > 0 || tagFilter) {
- SearchHistory.add(q);
- performAdvancedSearch(q, vault, tagFilter);
- } else {
- SearchChips.clear();
- showWelcome();
- }
- }
-
- // ---------------------------------------------------------------------------
- // Safe CDN helpers
- // ---------------------------------------------------------------------------
-
- /**
- * Debounced icon creation — batches multiple rapid calls into one
- * DOM scan to avoid excessive reflows when building large trees.
- */
- function safeCreateIcons() {
- if (typeof lucide === "undefined" || !lucide.createIcons) return;
- if (_iconDebounceTimer) return; // already scheduled
- _iconDebounceTimer = requestAnimationFrame(() => {
- _iconDebounceTimer = null;
- try {
- lucide.createIcons();
- } catch (e) {
- /* CDN not loaded */
- }
- });
- }
-
- /** Force-flush icon creation immediately (use sparingly). */
- function flushIcons() {
- if (_iconDebounceTimer) {
- cancelAnimationFrame(_iconDebounceTimer);
- _iconDebounceTimer = null;
- }
- 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 */
- }
- }
- }
-
- // ---------------------------------------------------------------------------
- // 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;
- },
- };
-
- // ---------------------------------------------------------------------------
- // Scroll Spy Manager
- // ---------------------------------------------------------------------------
-
- 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 = [];
- },
- };
-
- // ---------------------------------------------------------------------------
- // Reading Progress Manager
- // ---------------------------------------------------------------------------
-
- 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%";
- },
- };
-
- // ---------------------------------------------------------------------------
- // Right Sidebar Manager
- // ---------------------------------------------------------------------------
-
- const RightSidebarManager = {
- init() {
- this.loadState();
- this.initToggle();
- this.initResize();
- },
-
- loadState() {
- // Always start closed on login/refresh — user must explicitly toggle TOC
- const savedWidth = localStorage.getItem("obsigate-right-sidebar-width");
-
- 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);
- },
- };
-
- // ---------------------------------------------------------------------------
- // Theme
- // ---------------------------------------------------------------------------
- function initTheme() {
- const saved = localStorage.getItem("obsigate-theme") || "dark";
- applyTheme(saved);
- }
-
- function applyTheme(theme) {
- document.documentElement.setAttribute("data-theme", theme);
- localStorage.setItem("obsigate-theme", theme);
-
- // Update theme button icon and label
- const themeBtn = document.getElementById("theme-toggle");
- const themeLabel = document.getElementById("theme-label");
- if (themeBtn && themeLabel) {
- const icon = themeBtn.querySelector("i");
- if (icon) {
- icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun");
- }
- themeLabel.textContent = theme === "dark" ? "Sombre" : "Clair";
- safeCreateIcons();
- }
-
- // Swap highlight.js theme
- const darkSheet = document.getElementById("hljs-theme-dark");
- const lightSheet = document.getElementById("hljs-theme-light");
- if (darkSheet && lightSheet) {
- darkSheet.disabled = theme !== "dark";
- lightSheet.disabled = theme !== "light";
- }
- }
-
- function toggleTheme() {
- const current = document.documentElement.getAttribute("data-theme");
- applyTheme(current === "dark" ? "light" : "dark");
- }
-
- function initHeaderMenu() {
- const menuBtn = document.getElementById("header-menu-btn");
- const menuDropdown = document.getElementById("header-menu-dropdown");
-
- if (!menuBtn || !menuDropdown) return;
-
- menuBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- menuBtn.classList.toggle("active");
- menuDropdown.classList.toggle("active");
- });
-
- // Close menu when clicking outside
- document.addEventListener("click", (e) => {
- if (!menuDropdown.contains(e.target) && e.target !== menuBtn) {
- menuBtn.classList.remove("active");
- menuDropdown.classList.remove("active");
- }
- });
-
- // Prevent menu from closing when clicking inside
- menuDropdown.addEventListener("click", (e) => {
- e.stopPropagation();
- });
- }
-
- function closeHeaderMenu() {
- const menuBtn = document.getElementById("header-menu-btn");
- const menuDropdown = document.getElementById("header-menu-dropdown");
- if (!menuBtn || !menuDropdown) return;
- menuBtn.classList.remove("active");
- menuDropdown.classList.remove("active");
- }
-
- // ---------------------------------------------------------------------------
- // Custom Dropdowns
- // ---------------------------------------------------------------------------
- function initCustomDropdowns() {
- document.querySelectorAll(".custom-dropdown").forEach((dropdown) => {
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- const options = dropdown.querySelectorAll(".custom-dropdown-option");
- const hiddenInput = dropdown.querySelector('input[type="hidden"]');
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
- const menu = dropdown.querySelector(".custom-dropdown-menu");
-
- if (!trigger) return;
-
- // Toggle dropdown
- trigger.addEventListener("click", (e) => {
- e.stopPropagation();
- const isOpen = dropdown.classList.contains("open");
-
- // Close all other dropdowns
- document.querySelectorAll(".custom-dropdown.open").forEach((d) => {
- if (d !== dropdown) d.classList.remove("open");
- });
-
- dropdown.classList.toggle("open", !isOpen);
- trigger.setAttribute("aria-expanded", !isOpen);
-
- // Position fixed menu for sidebar dropdowns
- if (!isOpen && dropdown.classList.contains("sidebar-dropdown") && menu) {
- const rect = trigger.getBoundingClientRect();
- menu.style.top = `${rect.bottom + 4}px`;
- menu.style.left = `${rect.left}px`;
- menu.style.width = `${rect.width}px`;
- }
- });
-
- // Handle option selection
- options.forEach((option) => {
- option.addEventListener("click", (e) => {
- e.stopPropagation();
- const value = option.getAttribute("data-value");
- const text = option.textContent;
-
- // Update hidden input
- if (hiddenInput) {
- hiddenInput.value = value;
- // Trigger change event
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
- }
-
- // Update selected text
- if (selectedText) {
- selectedText.textContent = text;
- }
-
- // Update visual selection
- options.forEach((opt) => opt.classList.remove("selected"));
- option.classList.add("selected");
-
- // Close dropdown
- dropdown.classList.remove("open");
- trigger.setAttribute("aria-expanded", "false");
- });
- });
- });
-
- // Close dropdowns when clicking outside
- document.addEventListener("click", () => {
- document.querySelectorAll(".custom-dropdown.open").forEach((dropdown) => {
- dropdown.classList.remove("open");
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- if (trigger) trigger.setAttribute("aria-expanded", "false");
- });
- });
- }
-
- // Helper to populate custom dropdown options
- function populateCustomDropdown(dropdownId, optionsList, defaultValue) {
- const dropdown = document.getElementById(dropdownId);
- if (!dropdown) return;
-
- const optionsContainer = dropdown.querySelector(".custom-dropdown-menu");
- const hiddenInput = dropdown.querySelector('input[type="hidden"]');
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
-
- if (!optionsContainer) return;
-
- // Clear existing options (keep the first one if it's the default)
- optionsContainer.innerHTML = "";
-
- // Add new options
- optionsList.forEach((opt) => {
- const li = document.createElement("li");
- li.className = "custom-dropdown-option";
- li.setAttribute("role", "option");
- li.setAttribute("data-value", opt.value);
- li.textContent = opt.text;
- if (opt.value === defaultValue) {
- li.classList.add("selected");
- if (selectedText) selectedText.textContent = opt.text;
- if (hiddenInput) hiddenInput.value = opt.value;
- }
- optionsContainer.appendChild(li);
- });
-
- // Re-initialize click handlers
- optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((option) => {
- option.addEventListener("click", (e) => {
- e.stopPropagation();
- const value = option.getAttribute("data-value");
- const text = option.textContent;
-
- if (hiddenInput) {
- hiddenInput.value = value;
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
- }
-
- if (selectedText) {
- selectedText.textContent = text;
- }
-
- optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((opt) => opt.classList.remove("selected"));
- option.classList.add("selected");
-
- dropdown.classList.remove("open");
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- if (trigger) trigger.setAttribute("aria-expanded", "false");
- });
- });
- }
-
- // ---------------------------------------------------------------------------
- // Toast notifications
- // ---------------------------------------------------------------------------
-
- /** Display a brief toast message at the bottom of the viewport. */
- function showToast(message, type) {
- console.log("showToast called with:", message, type);
- type = type || "info";
- let container = document.getElementById("toast-container");
- if (!container) {
- container = document.createElement("div");
- container.id = "toast-container";
- container.className = "toast-container";
- container.setAttribute("aria-live", "polite");
- document.body.appendChild(container);
- }
- var toast = document.createElement("div");
- toast.className = "toast toast-" + type;
- toast.textContent = message;
- container.appendChild(toast);
- // Trigger entrance animation
- requestAnimationFrame(function () {
- toast.classList.add("show");
- });
- setTimeout(function () {
- toast.classList.remove("show");
- toast.addEventListener("transitionend", function () {
- toast.remove();
- });
- }, 3500);
- }
-
- // ---------------------------------------------------------------------------
- // API helpers
- // ---------------------------------------------------------------------------
-
- /**
- * Fetch JSON from an API endpoint with optional AbortSignal support.
- * Surfaces errors to the user via toast instead of silently failing.
- *
- * @param {string} path - API URL path.
- * @param {object} [opts] - Fetch options (may include signal).
- * @returns {Promise} Parsed JSON response.
- */
- async function api(path, opts) {
- var res;
- try {
- // Inject auth header if authenticated
- const authHeaders = AuthManager.getAuthHeaders();
- const mergedOpts = opts || {};
- // Auto-set Content-Type for JSON bodies
- if (mergedOpts.body && typeof mergedOpts.body === "string" && !mergedOpts.headers?.["Content-Type"]) {
- mergedOpts.headers = { ...mergedOpts.headers, "Content-Type": "application/json" };
- }
- if (authHeaders) {
- mergedOpts.headers = { ...mergedOpts.headers, ...authHeaders };
- }
- mergedOpts.credentials = "include";
- res = await fetch(path, mergedOpts);
- } catch (err) {
- if (err.name === "AbortError") throw err; // let callers handle abort
- showToast("Erreur réseau — vérifiez votre connexion", "error");
- throw err;
- }
- if (res.status === 401 && AuthManager._authEnabled) {
- // Token expired — try refresh
- try {
- await AuthManager.refreshAccessToken();
- // Retry the request with new token
- const retryHeaders = AuthManager.getAuthHeaders();
- const retryOpts = opts || {};
- retryOpts.headers = { ...retryOpts.headers, ...retryHeaders };
- retryOpts.credentials = "include";
- res = await fetch(path, retryOpts);
- } catch (refreshErr) {
- AuthManager.clearSession();
- AuthManager.showLoginScreen();
- throw new Error("Session expirée");
- }
- }
- if (!res.ok) {
- var detail = "";
- try {
- var body = await res.json();
- detail = body.detail || "";
- } catch (_) {
- /* no json body */
- }
- showToast(detail || "Erreur API : " + res.status, "error");
- throw new Error(detail || "API error: " + res.status);
- }
- return res.json();
- }
-
- // ---------------------------------------------------------------------------
- // AuthManager — Authentication state & token management
- // ---------------------------------------------------------------------------
-
- const AuthManager = {
- ACCESS_TOKEN_KEY: "obsigate_access_token",
- TOKEN_EXPIRY_KEY: "obsigate_token_expiry",
- USER_KEY: "obsigate_user",
- _authEnabled: false,
-
- // ── Token storage (sessionStorage) ─────────────────────────────
-
- saveToken(tokenData) {
- const expiresAt = Date.now() + tokenData.expires_in * 1000;
- sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token);
- sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString());
- if (tokenData.user) {
- sessionStorage.setItem(this.USER_KEY, JSON.stringify(tokenData.user));
- }
- },
-
- getToken() {
- return sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
- },
-
- getUser() {
- const raw = sessionStorage.getItem(this.USER_KEY);
- return raw ? JSON.parse(raw) : null;
- },
-
- isTokenExpired() {
- const expiry = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY);
- if (!expiry) return true;
- // Renew 60s before expiration
- return Date.now() > parseInt(expiry) - 60000;
- },
-
- clearSession() {
- sessionStorage.removeItem(this.ACCESS_TOKEN_KEY);
- sessionStorage.removeItem(this.TOKEN_EXPIRY_KEY);
- sessionStorage.removeItem(this.USER_KEY);
- },
-
- getAuthHeaders() {
- const token = this.getToken();
- if (!token || !this._authEnabled) return null;
- return { Authorization: "Bearer " + token };
- },
-
- // ── API calls ──────────────────────────────────────────────────
-
- async login(username, password, rememberMe) {
- const response = await fetch("/api/auth/login", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
- body: JSON.stringify({ username, password, remember_me: rememberMe || false }),
- });
- if (!response.ok) {
- const err = await response.json();
- throw new Error(err.detail || "Erreur de connexion");
- }
- const data = await response.json();
- this.saveToken(data);
- return data.user;
- },
-
- async logout() {
- try {
- const token = this.getToken();
- await fetch("/api/auth/logout", {
- method: "POST",
- headers: token ? { Authorization: "Bearer " + token } : {},
- credentials: "include",
- });
- } catch (e) {
- /* continue even if API fails */
- }
- this.clearSession();
- this.showLoginScreen();
- },
-
- async refreshAccessToken() {
- const response = await fetch("/api/auth/refresh", {
- method: "POST",
- credentials: "include",
- });
- if (!response.ok) {
- this.clearSession();
- throw new Error("Session expirée");
- }
- const data = await response.json();
- const expiry = Date.now() + data.expires_in * 1000;
- sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token);
- sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString());
- return data.access_token;
- },
-
- // ── UI controls ────────────────────────────────────────────────
-
- showLoginScreen() {
- const app = document.getElementById("app");
- const login = document.getElementById("login-screen");
- if (app) app.classList.add("hidden");
- if (login) {
- login.classList.remove("hidden");
- const usernameInput = document.getElementById("login-username");
- if (usernameInput) usernameInput.focus();
- }
- },
-
- showApp() {
- const login = document.getElementById("login-screen");
- const app = document.getElementById("app");
- if (login) login.classList.add("hidden");
- if (app) app.classList.remove("hidden");
- this.renderUserMenu();
- },
-
- renderUserMenu() {
- const user = this.getUser();
- const userMenu = document.getElementById("user-menu");
- if (!userMenu) return;
- if (!user || !this._authEnabled) {
- userMenu.innerHTML = "";
- return;
- }
- userMenu.innerHTML = '' + (user.display_name || user.username) + " " + ' ';
- safeCreateIcons();
-
- const logoutBtn = document.getElementById("logout-btn");
- if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout());
-
- const adminRow = document.getElementById("admin-menu-row");
- if (adminRow) {
- if (user.role === "admin") {
- adminRow.classList.remove("hidden");
- // Important: use an inline function to ensure we don't bind multiple identical listeners on rerenders, or clean up before
- adminRow.onclick = () => {
- closeHeaderMenu();
- AdminPanel.show();
- };
- } else {
- adminRow.classList.add("hidden");
- }
- }
- },
-
- // ── Initialization ──────────────────────────────────────────────
-
- async checkAuthStatus() {
- try {
- const res = await fetch("/api/auth/status");
- const data = await res.json();
- this._authEnabled = data.auth_enabled;
- return data;
- } catch (e) {
- this._authEnabled = false;
- return { auth_enabled: false };
- }
- },
-
- async initAuth() {
- const status = await this.checkAuthStatus();
- if (!status.auth_enabled) {
- // Auth disabled — show app immediately
- this.showApp();
- return true;
- }
-
- // Auth enabled — check for existing session
- if (this.getToken() && !this.isTokenExpired()) {
- this.showApp();
- return true;
- }
-
- // Try silent refresh
- try {
- await this.refreshAccessToken();
- // Fetch user info
- const token = this.getToken();
- const res = await fetch("/api/auth/me", {
- headers: { Authorization: "Bearer " + token },
- credentials: "include",
- });
- if (res.ok) {
- const user = await res.json();
- sessionStorage.setItem(this.USER_KEY, JSON.stringify(user));
- this.showApp();
- return true;
- }
- } catch (e) {
- /* silent refresh failed */
- }
-
- // No valid session — show login
- this.showLoginScreen();
- return false;
- },
- };
-
- // ---------------------------------------------------------------------------
- // Login form handler
- // ---------------------------------------------------------------------------
-
- function initLoginForm() {
- const form = document.getElementById("login-form");
- if (!form) return;
-
- form.addEventListener("submit", async (e) => {
- e.preventDefault();
- const username = document.getElementById("login-username").value;
- const password = document.getElementById("login-password").value;
- const rememberMe = document.getElementById("remember-me").checked;
- const errorEl = document.getElementById("login-error");
- const btn = document.getElementById("login-btn");
-
- btn.disabled = true;
- btn.querySelector(".btn-spinner").classList.remove("hidden");
- btn.querySelector(".btn-text").textContent = "Connexion...";
- errorEl.classList.add("hidden");
-
- try {
- await AuthManager.login(username, password, rememberMe);
- AuthManager.showApp();
- // Load app data after successful login
- try {
- await Promise.all([loadVaults(), loadTags()]);
- // Start SSE sync now that auth cookie is set
- IndexUpdateManager.connect();
- // Show dashboard
- showWelcome();
- } catch (err) {
- console.error("Failed to load data after login:", err);
- }
- safeCreateIcons();
- } catch (err) {
- errorEl.textContent = err.message;
- errorEl.classList.remove("hidden");
- document.getElementById("login-password").value = "";
- document.getElementById("login-password").focus();
- } finally {
- btn.disabled = false;
- btn.querySelector(".btn-spinner").classList.add("hidden");
- btn.querySelector(".btn-text").textContent = "Se connecter";
- }
- });
-
- // Toggle password visibility
- const toggleBtn = document.getElementById("toggle-password");
- if (toggleBtn) {
- toggleBtn.addEventListener("click", () => {
- const input = document.getElementById("login-password");
- input.type = input.type === "password" ? "text" : "password";
- });
- }
- }
-
- // ---------------------------------------------------------------------------
- // Admin Panel — User management (admin only)
- // ---------------------------------------------------------------------------
-
- const AdminPanel = {
- _modal: null,
- _allVaults: [],
-
- show() {
- this._createModal();
- this._modal.classList.add("active");
- this._loadUsers();
- },
-
- hide() {
- if (this._modal) this._modal.classList.remove("active");
- },
-
- _createModal() {
- if (this._modal) return;
- this._modal = document.createElement("div");
- this._modal.className = "editor-modal";
- this._modal.id = "admin-modal";
- this._modal.innerHTML = `
-
-
-
-
- + Nouvel utilisateur
-
-
-
-
- `;
- document.body.appendChild(this._modal);
- safeCreateIcons();
-
- document.getElementById("admin-close").addEventListener("click", () => this.hide());
- document.getElementById("admin-add-user").addEventListener("click", () => this._showUserForm(null));
- },
-
- async _loadUsers() {
- try {
- const users = await api("/api/auth/admin/users");
- // Also load available vaults
- try {
- const vaultsData = await api("/api/vaults");
- this._allVaults = vaultsData.map((v) => v.name);
- } catch (e) {
- this._allVaults = [];
- }
- this._renderUsers(users);
- } catch (err) {
- document.getElementById("admin-users-list").innerHTML = 'Erreur : ' + err.message + "
";
- }
- },
-
- _renderUsers(users) {
- const container = document.getElementById("admin-users-list");
- if (!users.length) {
- container.innerHTML = 'Aucun utilisateur.
';
- return;
- }
- let html = '' + "Utilisateur Rôle Vaults Statut Dernière connexion Actions " + " ";
- users.forEach((u) => {
- const vaults = u.vaults.includes("*") ? "Toutes" : u.vaults.join(", ") || "Aucune";
- const status = u.active ? "✅" : "🔴";
- const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : "Jamais";
- html +=
- "" +
- "" +
- u.username +
- " " +
- (u.display_name && u.display_name !== u.username ? "" + u.display_name + " " : "") +
- " " +
- '' +
- u.role +
- " " +
- '' +
- vaults +
- " " +
- "" +
- status +
- " " +
- "" +
- lastLogin +
- " " +
- '' +
- '✏️ ' +
- '🗑️ ' +
- " ";
- });
- html += "
";
- container.innerHTML = html;
-
- // Bind action buttons
- container.querySelectorAll('[data-action="edit"]').forEach((btn) => {
- btn.addEventListener("click", () => {
- const user = users.find((u) => u.username === btn.dataset.username);
- if (user) this._showUserForm(user);
- });
- });
- container.querySelectorAll('[data-action="delete"]').forEach((btn) => {
- btn.addEventListener("click", () => this._deleteUser(btn.dataset.username));
- });
- },
-
- _showUserForm(user) {
- const isEdit = !!user;
- const title = isEdit ? "Modifier : " + user.username : "Nouvel utilisateur";
- const vaultCheckboxes = this._allVaults
- .map((v) => {
- const checked = user && (user.vaults.includes(v) || user.vaults.includes("*")) ? "checked" : "";
- return '" + v + " ";
- })
- .join("");
- const allVaultsChecked = user && user.vaults.includes("*") ? "checked" : "";
-
- // Create form modal overlay
- const overlay = document.createElement("div");
- overlay.className = "admin-form-overlay";
- overlay.innerHTML = `
-
- `;
- this._modal.appendChild(overlay);
-
- document.getElementById("admin-form-cancel").addEventListener("click", () => overlay.remove());
-
- document.getElementById("admin-user-form").addEventListener("submit", async (e) => {
- e.preventDefault();
- const form = e.target;
- const allVaults = document.getElementById("admin-all-vaults").checked;
- const selectedVaults = allVaults ? ["*"] : Array.from(form.querySelectorAll('input[name="vault"]:checked')).map((cb) => cb.value);
-
- try {
- if (isEdit) {
- const updates = {
- display_name: form.display_name.value || null,
- role: form.role.value,
- vaults: selectedVaults,
- };
- if (form.password.value) updates.password = form.password.value;
- const activeCheckbox = form.querySelector('input[name="active"]');
- if (activeCheckbox) updates.active = activeCheckbox.checked;
- await api("/api/auth/admin/users/" + user.username, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(updates),
- });
- } else {
- await api("/api/auth/admin/users", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- username: form.username.value,
- password: form.password.value,
- display_name: form.display_name.value || null,
- role: form.role.value,
- vaults: selectedVaults,
- }),
- });
- }
- overlay.remove();
- this._loadUsers();
- showToast(isEdit ? "Utilisateur modifié" : "Utilisateur créé", "success");
- } catch (err) {
- showToast(err.message, "error");
- }
- });
- },
-
- async _deleteUser(username) {
- const currentUser = AuthManager.getUser();
- if (currentUser && currentUser.username === username) {
- showToast("Impossible de supprimer son propre compte", "error");
- return;
- }
- if (!confirm("Supprimer l'utilisateur \"" + username + '" ?')) return;
- try {
- await api("/api/auth/admin/users/" + username, { method: "DELETE" });
- this._loadUsers();
- showToast("Utilisateur supprimé", "success");
- } catch (err) {
- showToast(err.message, "error");
- }
- },
- };
-
- // ---------------------------------------------------------------------------
- // Sidebar toggle (desktop)
- // ---------------------------------------------------------------------------
- function initSidebarToggle() {
- const toggleBtn = document.getElementById("sidebar-toggle-btn");
- const sidebar = document.getElementById("sidebar");
- const resizeHandle = document.getElementById("sidebar-resize-handle");
-
- if (!toggleBtn || !sidebar || !resizeHandle) return;
-
- // Restore saved state
- const savedState = localStorage.getItem("obsigate-sidebar-hidden");
- if (savedState === "true") {
- sidebar.classList.add("hidden");
- resizeHandle.classList.add("hidden");
- toggleBtn.classList.add("active");
- }
-
- toggleBtn.addEventListener("click", () => {
- const isHidden = sidebar.classList.toggle("hidden");
- resizeHandle.classList.toggle("hidden", isHidden);
- toggleBtn.classList.toggle("active", isHidden);
- localStorage.setItem("obsigate-sidebar-hidden", isHidden ? "true" : "false");
- });
- }
-
- // ---------------------------------------------------------------------------
- // Mobile sidebar
- // ---------------------------------------------------------------------------
- function initMobile() {
- const hamburger = document.getElementById("hamburger-btn");
- const overlay = document.getElementById("sidebar-overlay");
- const sidebar = document.getElementById("sidebar");
-
- hamburger.addEventListener("click", () => {
- sidebar.classList.toggle("mobile-open");
- overlay.classList.toggle("active");
- });
-
- overlay.addEventListener("click", () => {
- sidebar.classList.remove("mobile-open");
- overlay.classList.remove("active");
- });
- }
-
- function closeMobileSidebar() {
- const sidebar = document.getElementById("sidebar");
- const overlay = document.getElementById("sidebar-overlay");
- if (sidebar) sidebar.classList.remove("mobile-open");
- if (overlay) overlay.classList.remove("active");
- }
-
- // ---------------------------------------------------------------------------
- // Vault context switching
- // ---------------------------------------------------------------------------
- function initVaultContext() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- if (!filter || !quickSelect) return;
-
- filter.addEventListener("change", async () => {
- await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" });
- });
-
- quickSelect.addEventListener("change", async () => {
- await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" });
- });
- }
-
- async function setSelectedVaultContext(vaultName, options) {
- selectedContextVault = vaultName;
- showingSource = false;
- cachedRawSource = null;
- syncVaultSelectors();
- await refreshSidebarForContext();
- await refreshTagsForContext();
-
- // Synchroniser le dashboard et les fichiers récents
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.load) {
- DashboardRecentWidget.load(vaultName);
- }
- if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
- DashboardBookmarkWidget.load(vaultName);
- }
- if (activeSidebarTab === "recent") {
- loadRecentFiles(vaultName === "all" ? null : vaultName);
- }
-
- showWelcome();
- if (options && options.focusVault && vaultName !== "all") {
- await focusVaultInSidebar(vaultName);
- }
- }
-
- function syncVaultSelectors() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- const recentFilter = document.getElementById("recent-vault-filter");
- const dashboardFilter = document.getElementById("dashboard-vault-filter");
- const contextText = document.getElementById("vault-context-text");
-
- if (filter) filter.value = selectedContextVault;
- if (quickSelect) quickSelect.value = selectedContextVault;
- if (recentFilter) recentFilter.value = selectedContextVault === "all" ? "" : selectedContextVault;
- if (dashboardFilter) dashboardFilter.value = selectedContextVault;
-
- // Mise à jour visuelle des dropdowns personnalisés
- updateCustomDropdownVisual("vault-filter-dropdown", selectedContextVault);
- updateCustomDropdownVisual("vault-quick-select-dropdown", selectedContextVault);
-
- // Update vault context indicator
- if (contextText) {
- contextText.textContent = selectedContextVault === "all" ? "All" : selectedContextVault;
- }
- }
-
- /**
- * Updates the visual state of a custom dropdown based on its current value.
- */
- function updateCustomDropdownVisual(dropdownId, value) {
- const dropdown = document.getElementById(dropdownId);
- if (!dropdown) return;
-
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
- const options = dropdown.querySelectorAll(".custom-dropdown-option");
-
- options.forEach((opt) => {
- const optValue = opt.getAttribute("data-value");
- if (optValue === value) {
- opt.classList.add("selected");
- if (selectedText) selectedText.textContent = opt.textContent;
- } else {
- opt.classList.remove("selected");
- }
- });
- }
-
- function scrollTreeItemIntoView(element, alignToTop) {
- if (!element) return;
- const scrollContainer = document.getElementById("sidebar-panel-vaults");
- if (!scrollContainer) return;
-
- const containerRect = scrollContainer.getBoundingClientRect();
- const elementRect = element.getBoundingClientRect();
- const isAbove = elementRect.top < containerRect.top;
- const isBelow = elementRect.bottom > containerRect.bottom;
-
- if (!isAbove && !isBelow && !alignToTop) return;
-
- const currentTop = scrollContainer.scrollTop;
- const offsetTop = element.offsetTop;
- const shouldCenter = alignToTop === "center";
- const centeredTop = Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height / 2) + (elementRect.height / 2));
- const targetTop = shouldCenter
- ? centeredTop
- : alignToTop
- ? Math.max(0, offsetTop - 60)
- : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35);
-
- scrollContainer.scrollTo({
- top: targetTop,
- behavior: "smooth",
- });
- }
-
- 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), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- safeCreateIcons();
- }
-
- async function focusVaultInSidebar(vaultName) {
- switchSidebarTab("vaults");
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (childContainer && childContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
- scrollTreeItemIntoView(vaultItem, false);
- }
-
- async function refreshTagsForContext() {
- const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
- const data = await api(`/api/tags${vaultParam}`);
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
- }
-
- // ---------------------------------------------------------------------------
- // Helper: Check if path should be displayed based on hideHiddenFiles setting
- // ---------------------------------------------------------------------------
- function shouldDisplayPath(path, vaultName) {
- // Get hideHiddenFiles setting for this vault (default: false = show all)
- const settings = vaultSettings[vaultName] || { hideHiddenFiles: false };
-
- if (!settings.hideHiddenFiles) {
- // Show all files
- return true;
- }
-
- // Check if any segment of the path starts with a dot (hidden)
- const segments = path.split("/").filter(Boolean);
- for (const segment of segments) {
- if (segment.startsWith(".")) {
- return false; // Hide this path
- }
- }
-
- return true; // Show this path
- }
-
- async function loadVaultSettings() {
- try {
- const settings = await api("/api/vaults/settings/all");
- vaultSettings = settings;
- } catch (err) {
- console.error("Failed to load vault settings:", err);
- vaultSettings = {};
- }
- }
-
- // ---------------------------------------------------------------------------
- // Sidebar — Vault tree
- // ---------------------------------------------------------------------------
- async function loadVaults() {
- const vaults = await api("/api/vaults");
- allVaults = vaults;
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- // Prepare dropdown options
- const dropdownOptions = [{ value: "all", text: "Tous les vaults" }, ...vaults.map((v) => ({ value: v.name, text: v.name }))];
-
- // Populate custom dropdowns
- populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all");
- populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all");
-
- // Populate standard selects
- _populateRecentVaultFilter();
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.populateVaultFilter) {
- DashboardRecentWidget.populateVaultFilter();
- }
-
- vaults.forEach((v) => {
- // Sidebar tree entry
- const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- syncVaultSelectors();
- safeCreateIcons();
- }
-
- /**
- * Refreshes the sidebar tree while preserving the expanded state of vaults and folders.
- * Optimized to avoid a full sidebar wipe and minimize visible loading states.
- */
- /**
- * Incrementally update a directory container without wiping existing DOM.
- * Only adds new items, removes deleted ones, and updates changed ones.
- */
- async function incrementalLoadDirectory(vaultName, dirPath, container) {
- let data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- // Server unavailable — keep existing content
- return;
- }
-
- // Build a map of existing DOM elements by path
- const existingItems = {};
- const existingChildren = {}; // path -> child container (for directories)
- for (let i = 0; i < container.children.length; i++) {
- const child = container.children[i];
- if (child.classList.contains("tree-item") && child.dataset.path) {
- existingItems[child.dataset.path] = child;
- // The next sibling should be the tree-children container for this directory
- if (i + 1 < container.children.length) {
- const next = container.children[i + 1];
- if (next.classList.contains("tree-children")) {
- existingChildren[child.dataset.path] = next;
- }
- }
- }
- }
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- if (!shouldDisplayPath(item.path, vaultName)) return;
-
- const existing = existingItems[item.path];
-
- if (existing) {
- // Item already exists — reuse it, but update text/badge if needed
- const textEl = existing.querySelector(".tree-item-text");
- const displayName = item.type === "file" && item.name.match(/\.md$/i)
- ? item.name.replace(/\.md$/i, "")
- : item.name;
- if (textEl && textEl.textContent !== displayName) {
- textEl.textContent = displayName;
- }
- // Update badge for directories
- if (item.type === "directory") {
- const badge = existing.querySelector(".badge-small");
- const newBadge = `(${item.children_count})`;
- if (badge && badge.textContent !== newBadge) {
- badge.textContent = newBadge;
- } else if (!badge) {
- existing.appendChild(smallBadge(item.children_count));
- }
- }
- fragment.appendChild(existing);
- // Also re-add the child container for directories
- if (item.type === "directory" && existingChildren[item.path]) {
- fragment.appendChild(existingChildren[item.path]);
- } else if (item.type === "directory") {
- // Directory existed but no child container — create one
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
- }
- } else {
- // New item — create it
- 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- TabManager.openPreview(vaultName, item.path);
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
- });
-
- fragment.appendChild(fileItem);
- }
- }
- });
-
- // Replace container content in a single batch operation to avoid flash
- container.textContent = "";
- container.appendChild(fragment);
- }
-
- async function refreshSidebarTreePreservingState() {
- // 1. Capture expanded states
- const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
- .filter((v) => {
- const children = document.getElementById(`vault-children-${v.dataset.vault}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((v) => v.dataset.vault);
-
- const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]"))
- .filter((item) => {
- const vault = item.dataset.vault;
- const path = item.dataset.path;
- const children = document.getElementById(`dir-${vault}-${path}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path }));
-
- const selectedItem = document.querySelector(".tree-item.path-selected");
- const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
-
- // 2. Soft update: vault names/counts without wiping the tree
- try {
- const vaults = await api("/api/vaults");
- allVaults = vaults;
- vaults.forEach((v) => {
- const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`);
- if (vItem) {
- const badge = vItem.querySelector(".badge-small");
- if (badge) badge.textContent = `(${v.file_count})`;
- }
- });
- } catch (e) {
- console.warn("Soft vault refresh failed, falling back to full reload", e);
- await loadVaults();
- return;
- }
-
- // 3. Incrementally update expanded vaults (no DOM wipe)
- for (const vName of expandedVaults) {
- const container = document.getElementById(`vault-children-${vName}`);
- if (container) {
- await incrementalLoadDirectory(vName, "", container);
- }
- }
-
- // 4. Incrementally update expanded directories (parents first, no DOM wipe)
- expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
- for (const dir of expandedDirs) {
- const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
- if (container) {
- try {
- await incrementalLoadDirectory(dir.vault, dir.path, container);
- container.classList.remove("collapsed");
- const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
- if (dItem) {
- const chev = dItem.querySelector("[data-lucide]");
- if (chev) chev.setAttribute("data-lucide", "chevron-down");
- }
- } catch (e) {
- console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
- }
- }
- }
-
- // 5. Restore selection
- if (selectedState) {
- await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false });
- }
-
- safeCreateIcons();
- }
-
- async function toggleVault(itemEl, vaultName, forceExpand) {
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!childContainer) return;
-
- scrollTreeItemIntoView(itemEl, false);
-
- const shouldExpand = forceExpand || childContainer.classList.contains("collapsed");
-
- if (shouldExpand) {
- // Expand — load children if empty
- if (childContainer.children.length === 0) {
- await loadDirectory(vaultName, "", childContainer);
- }
- childContainer.classList.remove("collapsed");
- // Swap chevron
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- safeCreateIcons();
- } else {
- childContainer.classList.add("collapsed");
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-right");
- safeCreateIcons();
- }
- }
-
- async function expandDirectoryInSidebar(vaultName, dirPath, dirItem) {
- const subContainer = document.getElementById(`dir-${vaultName}-${dirPath}`);
- if (!subContainer) return null;
-
- if (subContainer.children.length === 0) {
- await loadDirectory(vaultName, dirPath, subContainer);
- }
-
- subContainer.classList.remove("collapsed");
- if (dirItem) {
- const chevron = dirItem.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- }
- safeCreateIcons();
- return subContainer;
- }
-
- async function focusPathInSidebar(vaultName, targetPath, options) {
- switchSidebarTab("vaults");
-
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
-
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
-
- const vaultContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!vaultContainer) return;
-
- if (vaultContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
-
- if (!targetPath) {
- // Clear any previous path selection
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- scrollTreeItemIntoView(vaultItem, options && options.alignToTop);
- return;
- }
-
- const segments = targetPath.split("/").filter(Boolean);
- let currentContainer = vaultContainer;
- let cumulativePath = "";
- let lastTargetItem = null;
-
- for (let index = 0; index < segments.length; index++) {
- cumulativePath += (cumulativePath ? "/" : "") + segments[index];
-
- let targetItem = null;
- try {
- targetItem = currentContainer.querySelector(`.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(cumulativePath)}"]`);
- } catch (e) {
- targetItem = null;
- }
-
- if (!targetItem) {
- return;
- }
-
- lastTargetItem = targetItem;
-
- const isLastSegment = index === segments.length - 1;
- if (!isLastSegment) {
- const nextContainer = await expandDirectoryInSidebar(vaultName, cumulativePath, targetItem);
- if (nextContainer) {
- currentContainer = nextContainer;
- }
- }
- }
-
- if (lastTargetItem && options && options.expandTarget) {
- await expandDirectoryInSidebar(vaultName, targetPath, lastTargetItem);
- }
-
- // Clear previous path selections and highlight the final target
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- if (lastTargetItem) {
- lastTargetItem.classList.add("path-selected");
- }
-
- scrollTreeItemIntoView(lastTargetItem, options && options.alignToTop);
- }
-
- function getParentDirectoryPath(filePath) {
- if (!filePath) return "";
- const segments = filePath.split("/").filter(Boolean);
- if (segments.length <= 1) return "";
- segments.pop();
- return segments.join("/");
- }
-
- function syncActiveFileTreeItem(vaultName, filePath) {
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
- if (!vaultName || !filePath) return;
- const selector = `.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(filePath)}"]`;
- try {
- const active = document.querySelector(selector);
- if (active) active.classList.add("active");
- } catch (e) {
- /* selector might fail on special chars */
- }
- }
-
- async function loadDirectory(vaultName, dirPath, container) {
- // Only show the loading spinner if the container is currently empty
- const isEmpty = container.children.length === 0;
- if (isEmpty) {
- container.innerHTML = '';
- }
-
- var data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- container.innerHTML = 'Erreur de chargement
';
- return;
- }
- container.innerHTML = "";
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(item.path, vaultName)) {
- return; // Skip this 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- TabManager.openPreview(vaultName, item.path);
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly);
- });
-
- fragment.appendChild(fileItem);
- }
- });
-
- container.appendChild(fragment);
- safeCreateIcons();
- }
-
- // ---------------------------------------------------------------------------
- // Sidebar filter
- // ---------------------------------------------------------------------------
- function initSidebarFilter() {
- const input = document.getElementById("sidebar-filter-input");
- const caseBtn = document.getElementById("sidebar-filter-case-btn");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
-
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
- clearTimeout(filterDebounce);
- filterDebounce = setTimeout(async () => {
- const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (hasText) {
- if (activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- } else {
- if (activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- }
- }, 220);
- });
-
- caseBtn.addEventListener("click", async () => {
- sidebarFilterCaseSensitive = !sidebarFilterCaseSensitive;
- caseBtn.classList.toggle("active");
- const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (input.value.trim()) {
- if (activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- }
- });
-
- clearBtn.addEventListener("click", async () => {
- input.value = "";
- clearBtn.style.display = "none";
- sidebarFilterCaseSensitive = false;
- caseBtn.classList.remove("active");
- clearTimeout(filterDebounce);
- if (activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- });
-
- clearBtn.style.display = "none";
- }
-
- async function performTreeSearch(query) {
- if (!query) {
- await restoreSidebarTree();
- return;
- }
-
- try {
- const vaultParam = selectedContextVault === "all" ? "all" : selectedContextVault;
- const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`;
- const data = await api(url);
- renderFilteredSidebarResults(query, data.results);
- } catch (err) {
- console.error("Tree search error:", err);
- renderFilteredSidebarResults(query, []);
- }
- }
-
- async function restoreSidebarTree() {
- await refreshSidebarForContext();
- if (currentVault) {
- focusPathInSidebar(currentVault, currentPath || "", { alignToTop: false }).catch(() => {});
- }
- }
-
- function renderFilteredSidebarResults(query, results) {
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- const grouped = new Map();
- results.forEach((result) => {
- if (!grouped.has(result.vault)) {
- grouped.set(result.vault, []);
- }
- grouped.get(result.vault).push(result);
- });
-
- if (grouped.size === 0) {
- container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")]));
- return;
- }
-
- grouped.forEach((entries, vaultName) => {
- entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" }));
-
- const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]);
- container.appendChild(vaultHeader);
-
- const resultsWrapper = el("div", { class: "filter-results-group" });
- entries.forEach((entry) => {
- const resultItem = el(
- "div",
- {
- class: `tree-item filter-result-item filter-result-${entry.type}`,
- "data-vault": entry.vault,
- "data-path": entry.path,
- "data-type": entry.type,
- },
- [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)],
- );
-
- const textWrap = el("div", { class: "filter-result-text" });
- const primary = el("div", { class: "filter-result-primary" });
- appendHighlightedText(primary, entry.name, query, sidebarFilterCaseSensitive);
- const secondary = el("div", { class: "filter-result-secondary" });
- appendHighlightedText(secondary, entry.path, query, sidebarFilterCaseSensitive);
- textWrap.appendChild(primary);
- textWrap.appendChild(secondary);
- resultItem.appendChild(textWrap);
-
- resultItem.addEventListener("click", async () => {
- const input = document.getElementById("sidebar-filter-input");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
- if (input) input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- await restoreSidebarTree();
- if (entry.type === "directory") {
- await focusPathInSidebar(entry.vault, entry.path, { alignToTop: true, expandTarget: true });
- } else {
- await TabManager.openPreview(entry.vault, entry.path);
- await focusPathInSidebar(entry.vault, getParentDirectoryPath(entry.path), { alignToTop: true, expandTarget: true });
- syncActiveFileTreeItem(entry.vault, entry.path);
- }
- closeMobileSidebar();
- });
-
- resultsWrapper.appendChild(resultItem);
- });
-
- container.appendChild(resultsWrapper);
- });
-
- flushIcons();
- }
-
- function filterSidebarTree(query) {
- const tree = document.getElementById("vault-tree");
- const items = tree.querySelectorAll(".tree-item");
- const containers = tree.querySelectorAll(".tree-children");
-
- if (!query) {
- items.forEach((item) => item.classList.remove("filtered-out"));
- containers.forEach((c) => {
- c.classList.remove("filtered-out");
- // Keep current collapsed state when clearing filter
- });
- return;
- }
-
- // First pass: mark all as filtered out
- items.forEach((item) => item.classList.add("filtered-out"));
- containers.forEach((c) => c.classList.add("filtered-out"));
-
- // Second pass: find matching items and mark them + ancestors + descendants
- const matchingItems = new Set();
-
- items.forEach((item) => {
- const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase();
- const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (text.includes(searchQuery)) {
- matchingItems.add(item);
- 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;
- }
-
- // If this is a directory (has a children container after it), show all descendants
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- // Recursively show all children in this container
- showAllDescendants(nextEl);
- }
- }
- });
-
- // Third pass: show items that are descendants of matching directories
- // and ensure their containers are visible
- matchingItems.forEach((item) => {
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- const children = nextEl.querySelectorAll(".tree-item");
- children.forEach((child) => child.classList.remove("filtered-out"));
- }
- });
- }
-
- function showAllDescendants(container) {
- const items = container.querySelectorAll(".tree-item");
- items.forEach((item) => {
- item.classList.remove("filtered-out");
- // If this item has children, also show them
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- }
- });
- // Also ensure all nested containers are visible
- const nestedContainers = container.querySelectorAll(".tree-children");
- nestedContainers.forEach((c) => {
- c.classList.remove("filtered-out");
- c.classList.remove("collapsed");
- });
- }
-
- function filterTagCloud(query) {
- const tags = document.querySelectorAll("#tag-cloud .tag-item");
- tags.forEach((tag) => {
- const text = sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase();
- const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (!query || text.includes(searchQuery)) {
- tag.classList.remove("filtered-out");
- } else {
- tag.classList.add("filtered-out");
- }
- });
- }
-
- // ---------------------------------------------------------------------------
- // Tag Filter Service
- // ---------------------------------------------------------------------------
- const TagFilterService = {
- defaultFilters: [
- { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true },
- { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true },
- { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true },
- ],
-
- getConfig() {
- const stored = localStorage.getItem("obsigate-tag-filters");
- if (stored) {
- try {
- return JSON.parse(stored);
- } catch (e) {
- return { tagFilters: this.defaultFilters };
- }
- }
- return { tagFilters: this.defaultFilters };
- },
-
- saveConfig(config) {
- localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
- },
-
- patternToRegex(pattern) {
- // 1. Escape ALL special regex characters
- // We use a broader set including * and .
- let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-
- // 2. Convert escaped '*' to '.*' (wildcard)
- regex = regex.replace(/\\\*/g, ".*");
-
- // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*'
- // We also handle optional whitespace around it to make it more user-friendly
- regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*");
-
- return regex;
- },
-
- isTagFiltered(tag) {
- const config = this.getConfig();
- const filters = config.tagFilters || this.defaultFilters;
- const tagWithHash = `#${tag}`;
-
- for (const filter of filters) {
- if (!filter.enabled) continue;
- try {
- // Robustly handle regex with or without ^/$
- let patternStr = filter.regex;
- if (!patternStr.startsWith("^")) patternStr = "^" + patternStr;
- if (!patternStr.endsWith("$")) patternStr = patternStr + "$";
-
- const regex = new RegExp(patternStr);
- if (regex.test(tagWithHash)) {
- return true;
- }
- } catch (e) {
- console.warn("Invalid regex:", filter.regex, e);
- }
- }
- return false;
- },
-
- filterTags(tags) {
- const filtered = {};
- Object.entries(tags).forEach(([tag, count]) => {
- if (!this.isTagFiltered(tag)) {
- filtered[tag] = count;
- }
- });
- return filtered;
- },
- };
-
- // ---------------------------------------------------------------------------
- // Tags
- // ---------------------------------------------------------------------------
- async function loadTags() {
- const data = await api("/api/tags");
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
- }
-
- function renderTagCloud(tags) {
- const cloud = document.getElementById("tag-cloud");
- cloud.innerHTML = "";
-
- const counts = Object.values(tags);
- if (counts.length === 0) return;
-
- const maxCount = Math.max(...counts);
- const minSize = 0.7;
- const maxSize = 1.25;
-
- Object.entries(tags).forEach(([tag, count]) => {
- const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0;
- const size = minSize + ratio * (maxSize - minSize);
- const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", () => searchByTag(tag));
- cloud.appendChild(tagEl);
- });
- }
-
- function addTagFilter(tag) {
- if (!selectedTags.includes(tag)) {
- selectedTags.push(tag);
- performTagSearch();
- }
- }
-
- function removeTagFilter(tag) {
- selectedTags = selectedTags.filter((t) => t !== tag);
- if (selectedTags.length > 0) {
- performTagSearch();
- } else {
- const input = document.getElementById("search-input");
- if (input.value.trim()) {
- performAdvancedSearch(input.value.trim(), document.getElementById("vault-filter").value, null);
- } else {
- showWelcome();
- }
- }
- }
-
- function performTagSearch() {
- const input = document.getElementById("search-input");
- const query = input.value.trim();
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, selectedTags.length > 0 ? selectedTags.join(",") : null);
- }
-
- function buildSearchResultsHeader(data, query, tagFilter) {
- const header = el("div", { class: "search-results-header" });
- const summaryText = el("span", { class: "search-results-summary-text" });
-
- if (query && tagFilter) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`;
- } else if (query) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}"`;
- } else if (tagFilter) {
- summaryText.textContent = `${data.count} fichier(s) avec les tags`;
- } else {
- summaryText.textContent = `${data.count} résultat(s)`;
- }
-
- header.appendChild(summaryText);
-
- if (selectedTags.length > 0) {
- const activeTags = el("div", { class: "search-results-active-tags" });
- selectedTags.forEach((tag) => {
- const removeBtn = el(
- "button",
- {
- class: "search-results-active-tag-remove",
- title: `Retirer ${tag} du filtre`,
- "aria-label": `Retirer ${tag} du filtre`,
- },
- [document.createTextNode("×")],
- );
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeTagFilter(tag);
- });
-
- const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
- activeTags.appendChild(chip);
- });
- header.appendChild(activeTags);
- }
-
- return header;
- }
-
- function searchByTag(tag) {
- addTagFilter(tag);
- }
-
- // ---------------------------------------------------------------------------
- // File viewer
- // ---------------------------------------------------------------------------
- async function openFile(vaultName, filePath) {
- currentVault = vaultName;
- currentPath = filePath;
- showingSource = false;
- cachedRawSource = null;
-
- // Highlight active
- syncActiveFileTreeItem(vaultName, filePath);
-
- // Show loading state while fetching
- const area = document.getElementById("content-area");
- area.innerHTML = '';
-
- try {
- const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
- const data = await api(url);
- renderFile(data);
- } catch (err) {
- area.innerHTML = 'Impossible de charger le fichier.
';
- }
- }
-
- async function renderBacklinksPanel(vault, path, container) {
- try {
- const data = await api(`/api/file/${encodeURIComponent(vault)}/backlinks?path=${encodeURIComponent(path)}`);
- if (!data.backlinks || data.backlinks.length === 0) return;
-
- const panel = el("div", { class: "backlinks-panel" });
- const header = el("div", { class: "backlinks-header" }, [
- icon("link", 14),
- document.createTextNode(` ${data.total} lien(s) entrant(s)`),
- ]);
- panel.appendChild(header);
-
- const list = el("div", { class: "backlinks-list" });
- data.backlinks.forEach((bl) => {
- const item = el("div", { class: "backlink-item" });
- const vaultBadge = el("span", { class: "backlink-vault" }, [document.createTextNode(bl.vault)]);
- const titleEl = el("span", { class: "backlink-title" }, [document.createTextNode(bl.title || bl.path.split("/").pop().replace(/\.md$/i, ""))]);
- item.appendChild(icon(getFileIcon(bl.path), 12));
- item.appendChild(vaultBadge);
- item.appendChild(titleEl);
- item.addEventListener("click", () => TabManager.openPreview(bl.vault, bl.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(bl.vault, bl.path); });
- list.appendChild(item);
- });
- panel.appendChild(list);
- container.appendChild(panel);
- } catch (err) {
- // Silently ignore — backlinks are optional
- console.debug("Backlinks fetch failed:", err);
- }
- }
-
- function renderFile(data) {
- const area = document.getElementById("content-area");
-
- // Handle unsupported (binary) files
- if (data.unsupported) {
- const sizeStr = data.size_bytes
- ? data.size_bytes < 1024 ? `${data.size_bytes} o`
- : data.size_bytes < 1048576 ? `${(data.size_bytes / 1024).toFixed(1)} Ko`
- : `${(data.size_bytes / 1048576).toFixed(1)} Mo`
- : "";
- area.innerHTML = `
-
-
-
${escapeHtml(data.path.split("/").pop())}
-
Ce fichier est binaire et ne peut pas être affiché.
- ${sizeStr ? `
Taille : ${sizeStr}
` : ""}
-
- Télécharger
-
-
`;
- lucide.createIcons();
- document.getElementById("unsupported-download-btn").addEventListener("click", () => {
- const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`;
- window.open(dlUrl, "_blank");
- });
- return;
- }
-
- // Breadcrumb
- const parts = data.path.split("/");
- const breadcrumbEls = [];
- breadcrumbEls.push(
- makeBreadcrumbSpan(data.vault, () => {
- focusPathInSidebar(data.vault, "", { alignToTop: "center" });
- }),
- );
- 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, () => {
- focusPathInSidebar(data.vault, p, { alignToTop: "center" });
- }),
- );
- } else {
- breadcrumbEls.push(
- makeBreadcrumbSpan(part.replace(/\.md$/i, ""), () => {
- focusPathInSidebar(data.vault, data.path, { alignToTop: "center" });
- }),
- );
- }
- });
-
- const breadcrumb = el("div", { class: "breadcrumb" }, breadcrumbEls);
-
- // Tags
- const tagsDiv = el("div", { class: "file-tags" });
- (data.tags || []).forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- t.addEventListener("click", () => searchByTag(tag));
- tagsDiv.appendChild(t);
- }
- });
-
- // Action buttons
- const copyBtn = el("button", { class: "btn-action", title: "Copier la source" }, [icon("copy", 14), document.createTextNode("Copier")]);
- copyBtn.addEventListener("click", async () => {
- try {
- // Fetch raw content if not already cached
- if (!cachedRawSource) {
- const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
- const rawData = await api(rawUrl);
- cachedRawSource = rawData.raw;
- }
- await navigator.clipboard.writeText(cachedRawSource);
- copyBtn.lastChild.textContent = "Copié !";
- setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500);
- } catch (err) {
- console.error("Copy error:", err);
- showToast("Erreur lors de la copie", "error");
- }
- });
-
- const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [icon("code", 14), document.createTextNode("Source")]);
-
- // MD download button
- const mdBtn = el("button", { class: "btn-action", title: "Télécharger en .md" }, [icon("file-text", 14), document.createTextNode(".md")]);
- mdBtn.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);
- });
-
- // PDF download button
- const pdfBtn = el("button", { class: "btn-action", title: "Télécharger en PDF" }, [icon("file", 14), document.createTextNode("PDF")]);
- pdfBtn.addEventListener("click", () => {
- const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf?path=${encodeURIComponent(data.path)}`;
- window.open(pdfUrl, "_blank");
- });
-
- const editBtn = el("button", { class: "btn-action", title: "Editer" }, [icon("edit", 14), document.createTextNode("Editer")]);
- editBtn.addEventListener("click", () => {
- openEditor(data.vault, data.path);
- });
-
- const forgeBtn = el("button", { class: "btn-action btn-forge", title: "Forge (nouvel editeur)" }, [icon("hammer", 14), document.createTextNode("Forge")]);
- forgeBtn.addEventListener("click", () => {
- var modal = document.getElementById("editor-modal");
- var titleEl = document.getElementById("editor-title");
- var bodyEl = document.getElementById("editor-body");
- titleEl.textContent = "Forge: " + data.path.split("/").pop();
- bodyEl.innerHTML = "";
- var iframe = document.createElement("iframe");
- iframe.id = "forge-iframe";
- iframe.src = "/editor-poc?vault=" + encodeURIComponent(data.vault) + "&path=" + encodeURIComponent(data.path) + "&_ts=" + Date.now();
- iframe.style.cssText = "width:100%;height:82vh;border:none;display:block;";
- bodyEl.appendChild(iframe);
- modal.classList.add("active");
- safeCreateIcons();
- });
-
- const openNewWindowBtn = el("button", { class: "btn-action", title: "Ouvrir dans une nouvelle fenetre" }, [icon("external-link", 14), document.createTextNode("pop-out")]);
- openNewWindowBtn.addEventListener("click", () => {
- const theme = document.documentElement.getAttribute('data-theme') || 'dark';
- const popoutUrl = `/popout/${encodeURIComponent(data.vault)}/${encodeURIComponent(data.path)}?theme=${theme}`;
- window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, "_")}`, "width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no");
- });
-
- const tocBtn = el("button", { class: "btn-action", id: "toc-toggle-btn", title: "Afficher/Masquer le sommaire" }, [icon("list", 14), document.createTextNode("TOC")]);
- tocBtn.addEventListener("click", () => {
- RightSidebarManager.toggle();
- });
-
- // Share button — check if already shared
- const shareBtn = el("button", { class: "btn-action btn-share", title: "Partager ce document" }, [icon("share-2", 14), document.createTextNode("Partager")]);
- // Check if already shared and color the button
- (async () => {
- try {
- const shares = await api("/api/shares");
- if (shares.some(s => s.vault === data.vault && s.path === data.path)) {
- shareBtn.classList.add("shared");
- shareBtn.title = "Document partagé — cliquer pour gérer";
- }
- } catch (e) { /* ignore */ }
- })();
- shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
-
- // Bookmark button — check if already bookmarked
- const bookmarkBtn = el("button", { class: "btn-action btn-bookmark", title: "Ajouter/Retirer des bookmarks" }, [icon("bookmark-plus", 14), document.createTextNode("Bookmark")]);
- // Check bookmark status and color the button
- (async () => {
- try {
- const bms = await api("/api/bookmarks");
- if (Array.isArray(bms) && bms.some(b => b.vault === data.vault && b.path === data.path)) {
- bookmarkBtn.classList.add("active");
- bookmarkBtn.title = "Retirer des bookmarks";
- }
- } catch (e) { /* ignore */ }
- })();
- bookmarkBtn.addEventListener("click", async () => {
- try {
- const res = await api("/api/bookmarks/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vault: data.vault, path: data.path, title: data.title }) });
- bookmarkBtn.classList.toggle("active", res.bookmarked);
- bookmarkBtn.title = res.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
- showToast(res.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");
- if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
-
- // Frontmatter — Accent Card
- let fmSection = null;
- if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
- fmSection = buildFrontmatterCard(data.frontmatter);
- }
-
- // Content container (rendered HTML)
- const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" });
- mdDiv.innerHTML = data.html;
-
- // Raw source container (hidden initially)
- const rawDiv = el("div", { class: "raw-source-view", id: "file-raw-content", style: "display:none" });
-
- // Source button toggle logic
- sourceBtn.addEventListener("click", async () => {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (!rendered || !raw) return;
-
- showingSource = !showingSource;
- if (showingSource) {
- sourceBtn.classList.add("active");
- if (!cachedRawSource) {
- const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
- const rawData = await api(rawUrl);
- cachedRawSource = rawData.raw;
- }
- raw.textContent = cachedRawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- } else {
- sourceBtn.classList.remove("active");
- rendered.style.display = "block";
- raw.style.display = "none";
- }
- });
-
- // Assemble
- area.innerHTML = "";
- area.appendChild(breadcrumb);
- area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn, sourceBtn, mdBtn, pdfBtn, editBtn, forgeBtn, openNewWindowBtn, tocBtn, shareBtn, bookmarkBtn])]));
- if (fmSection) area.appendChild(fmSection);
- area.appendChild(mdDiv);
- area.appendChild(rawDiv);
-
- // Backlinks panel
- if (data.is_markdown) {
- renderBacklinksPanel(data.vault, data.path, area);
- }
-
- // Highlight code blocks
- area.querySelectorAll("pre code").forEach((block) => {
- safeHighlight(block);
- });
-
- // Wire up wikilinks
- area.querySelectorAll(".wikilink").forEach((link) => {
- link.addEventListener("click", (e) => {
- e.preventDefault();
- const v = link.getAttribute("data-vault");
- const p = link.getAttribute("data-path");
- if (v && p) openFile(v, p);
- });
- });
-
- safeCreateIcons();
- area.scrollTop = 0;
-
- // Initialize outline/TOC for this document
- OutlineManager.init();
- }
-
- // ---------------------------------------------------------------------------
- // Recent files
- // ---------------------------------------------------------------------------
- let _recentRefreshTimer = null;
- let _recentTimestampTimer = null;
- let _recentFilesCache = [];
-
- // ---------------------------------------------------------------------------
- // Dashboard Recent Files Widget
- // ---------------------------------------------------------------------------
- // ── Dashboard Stats Widget ──
- const DashboardStatsWidget = {
- async load() {
- const grid = document.getElementById("dashboard-stats-grid");
- if (!grid) return;
- grid.innerHTML = 'Chargement...
';
- try {
- const data = await api("/api/dashboard");
- this.render(data);
- } catch (err) {
- grid.innerHTML = `Erreur: ${escapeHtml(err.message)}
`;
- }
- },
- render(data) {
- const grid = document.getElementById("dashboard-stats-grid");
- if (!grid) return;
- const fmtSize = (bytes) => bytes < 1024 ? `${bytes} o` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)} Ko` : bytes < 1073741824 ? `${(bytes/1048576).toFixed(1)} Mo` : `${(bytes/1073741824).toFixed(1)} Go`;
- const items = [
- { icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() },
- { icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() },
- { icon: "hard-drive", label: "Taille totale", value: fmtSize(data.total_size_bytes) },
- { icon: "folder-open", label: "Vaults", value: data.vaults.length.toString() },
- ];
- grid.innerHTML = items.map(i => `
-
-
- ${i.value}
- ${i.label}
-
- `).join("");
- safeCreateIcons();
- }
- };
-
- // ── Dashboard Shared Widget ──
- const DashboardSharedWidget = {
- async load() {
- const grid = document.getElementById("dashboard-shared-grid");
- const empty = document.getElementById("dashboard-shared-empty");
- if (!grid) return;
- try {
- const shares = await api("/api/shares");
- if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
- if (empty) empty.style.display = "none";
- grid.innerHTML = shares.map(s => `
-
-
-
- ${s.access_count || 0} vue(s)
- ${s.expires_at ? `Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")} ` : ""}
-
-
- 📋 Copier
- 📂 Ouvrir
- 🗑
-
-
- `).join("");
- lucide.createIcons();
- grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- const url = b.dataset.url;
- try { await navigator.clipboard.writeText(url); } catch { const ta = document.createElement("textarea"); ta.value=url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
- showToast("Lien copié !", "success");
- }));
- grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
- e.stopPropagation();
- const card = b.closest(".shared-card");
- if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
- }));
- grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
- showToast("Partage révoqué", "success");
- this.load();
- }));
- grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
- TabManager.openPreview(card.dataset.vault, card.dataset.path);
- }));
- } catch (err) { if (empty) empty.style.display = ""; }
- }
- };
-
- // ── Dashboard Conflicts Widget ──
- const DashboardConflictsWidget = {
- async load() {
- const container = document.getElementById("dashboard-conflicts-container");
- if (!container) return;
- try {
- const data = await api("/api/conflicts");
- if (data.total === 0) { container.innerHTML = ""; return; }
- this.render(data.conflicts, container);
- } catch (err) { container.innerHTML = ""; }
- },
- render(conflicts, container) {
- container.innerHTML = `
-
-
-
- ${conflicts.map(c => `
-
-
- ${escapeHtml(c.vault)}
- ${escapeHtml(c.conflict_path.split("/").pop())}
- Conflit du ${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}
-
-
- Garder l'original
- Garder le conflit
-
-
- `).join("")}
-
-
`;
- lucide.createIcons();
- container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
- container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
- },
- async _resolve(d, action) {
- try {
- await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
- showToast("Conflit résolu", "success");
- this.load();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- }
- };
-
- const DashboardRecentWidget = {
- _cache: [],
- _currentFilter: "",
-
- async load(vaultFilter = "") {
- const v = vaultFilter || selectedContextVault || "all";
- this._currentFilter = v;
- this.showLoading();
-
- let url = "/api/recent?mode=opened";
- if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
-
- try {
- const data = await api(url);
- this._cache = data.files || [];
- this.render();
- } catch (err) {
- console.error("Dashboard: Failed to load recent files:", err);
- this.showError();
- }
- },
-
- async toggleBookmark(vault, path, title, card) {
- try {
- const data = await api("/api/bookmarks/toggle", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ vault, path, title }),
- });
-
- // Refresh both widgets to keep sync
- DashboardBookmarkWidget.load();
-
- // Update current card icon if it exists
- if (card) {
- const btn = card.querySelector(".dashboard-card-bookmark-btn");
- if (btn) {
- btn.classList.toggle("active", data.bookmarked);
- const icon = btn.querySelector("i");
- if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
- safeCreateIcons();
- }
- }
-
- // Check if we need to refresh the current list to reflect bookmark status across all cards
- // To avoid flickering, just update the cache and re-render if needed or do a silent refresh
- this._cache.forEach(f => {
- if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
- });
- } catch (err) {
- console.error("Failed to toggle bookmark:", err);
- showToast("Erreur lors de l'épinglage", "error");
- }
- },
-
- showLoading() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (grid) grid.innerHTML = "";
- if (loading) loading.classList.add("active");
- if (empty) empty.classList.add("hidden");
- if (count) count.textContent = "";
- },
-
- render() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (loading) loading.classList.remove("active");
-
- if (!this._cache || this._cache.length === 0) {
- this.showEmpty();
- return;
- }
-
- if (empty) empty.classList.add("hidden");
- if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`;
-
- if (!grid) return;
- grid.innerHTML = "";
-
- this._cache.forEach((f, index) => {
- const card = this._createCard(f, index);
- grid.appendChild(card);
- });
-
- safeCreateIcons();
- },
-
- _createCard(file, index) {
- const card = document.createElement("div");
- card.className = "dashboard-card";
- card.setAttribute("data-vault", file.vault);
- card.setAttribute("data-path", file.path);
- card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
-
- // Header with icon and vault badge
- const header = document.createElement("div");
- header.className = "dashboard-card-header";
-
- const iconContainer = document.createElement("div");
- iconContainer.className = "dashboard-card-icon";
- const fileIconName = getFileIcon(file.path);
- try {
- iconContainer.appendChild(icon(fileIconName, 24));
- } catch (e) {
- console.error("Error creating icon:", fileIconName, e);
- // Fallback to default file icon
- iconContainer.appendChild(icon("file", 24));
- }
-
- const badge = document.createElement("span");
- badge.className = "dashboard-vault-badge";
- badge.textContent = file.vault;
-
- const bookmarkBtn = document.createElement("button");
- bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
- bookmarkBtn.title = file.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
- bookmarkBtn.innerHTML = ` `;
-
- bookmarkBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- this.toggleBookmark(file.vault, file.path, file.title, card);
- });
-
- header.appendChild(iconContainer);
- header.appendChild(badge);
- header.appendChild(bookmarkBtn);
- card.appendChild(header);
-
- // Title
- const title = document.createElement("h3");
- title.className = "dashboard-card-title";
- title.textContent = file.title || file.path.split("/").pop();
- title.title = file.title || file.path;
- card.appendChild(title);
-
- // Path (compact)
- const pathParts = file.path.split("/");
- if (pathParts.length > 1) {
- const path = document.createElement("div");
- path.className = "dashboard-card-path";
- path.textContent = pathParts.slice(0, -1).join(" / ");
- path.title = file.path;
- card.appendChild(path);
- }
-
- // Footer with time and tags
- const footer = document.createElement("div");
- footer.className = "dashboard-card-footer";
-
- const time = document.createElement("span");
- time.className = "dashboard-card-time";
- time.innerHTML = ` ${file.mtime_human || this._humanizeDelta(file.mtime)}`;
-
- footer.appendChild(time);
-
- // Tags
- if (file.tags && file.tags.length > 0) {
- const tags = document.createElement("div");
- tags.className = "dashboard-card-tags";
- file.tags.slice(0, 3).forEach((tag) => {
- const tagEl = document.createElement("span");
- tagEl.className = "tag-pill";
- tagEl.textContent = tag;
- tags.appendChild(tagEl);
- });
- footer.appendChild(tags);
- }
-
- card.appendChild(footer);
-
- // Click handler
- card.addEventListener("click", () => {
- openFile(file.vault, file.path);
- });
-
- return card;
- },
-
- showEmpty() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (grid) grid.innerHTML = "";
- if (loading) loading.classList.remove("active");
- if (empty) empty.classList.remove("hidden");
- if (count) count.textContent = "0 fichiers";
- safeCreateIcons();
- },
-
- showError() {
- this.showEmpty();
- const empty = document.getElementById("dashboard-recent-empty");
- if (empty) {
- const msg = empty.querySelector("span");
- if (msg) msg.textContent = "Erreur de chargement";
- }
- },
-
- _humanizeDelta(mtime) {
- const delta = Date.now() / 1000 - mtime;
- if (delta < 60) return "à l'instant";
- if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
- if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
- if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
- return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
- },
-
- populateVaultFilter() {
- const select = document.getElementById("dashboard-vault-filter");
- if (!select) return;
-
- // Keep first option "Tous les vaults"
- while (select.options.length > 1) select.remove(1);
-
- if (typeof allVaults !== "undefined" && Array.isArray(allVaults)) {
- allVaults.forEach((v) => {
- const opt = document.createElement("option");
- opt.value = v.name;
- opt.textContent = v.name;
- select.appendChild(opt);
- });
- }
- syncVaultSelectors();
- },
-
- init() {
- const select = document.getElementById("dashboard-vault-filter");
- if (select) {
- select.addEventListener("change", async () => {
- await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
- });
- }
-
- this.populateVaultFilter();
- },
- };
-
- // ---------------------------------------------------------------------------
- // Dashboard Bookmarks Widget
- // ---------------------------------------------------------------------------
- const DashboardBookmarkWidget = {
- _cache: [],
- _currentFilter: "",
-
- async load(vaultFilter = "") {
- const v = vaultFilter || selectedContextVault || "all";
- this._currentFilter = v;
- this.showLoading();
-
- let url = "/api/bookmarks";
- if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
-
- try {
- const data = await api(url);
- this._cache = data.files || [];
- this.render();
- } catch (err) {
- console.error("Dashboard: Failed to load bookmarks:", err);
- this.showEmpty();
- }
- },
-
- showLoading() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- const section = document.getElementById("dashboard-bookmarks-section");
-
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.add("hidden");
- },
-
- render() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- const section = document.getElementById("dashboard-bookmarks-section");
-
- if (!this._cache || this._cache.length === 0) {
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.remove("hidden");
- return;
- }
-
- if (empty) empty.classList.add("hidden");
- if (!grid) return;
- grid.innerHTML = "";
-
- this._cache.forEach((f, idx) => {
- const card = DashboardRecentWidget._createCard(f, idx);
- grid.appendChild(card);
- });
-
- safeCreateIcons();
- },
-
- showEmpty() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.remove("hidden");
- }
- };
-
- async function loadRecentFiles(vaultFilter) {
- const listEl = document.getElementById("recent-list");
- const emptyEl = document.getElementById("recent-empty");
- if (!listEl) return;
-
- let url = "/api/recent?mode=modified";
- if (vaultFilter) url += `&vault=${encodeURIComponent(vaultFilter)}`;
- try {
- const data = await api(url);
- _recentFilesCache = data.files || [];
- renderRecentList(_recentFilesCache);
- } catch (err) {
- console.error("Failed to load recent files:", err);
- listEl.innerHTML = "";
- if (emptyEl) {
- emptyEl.classList.remove("hidden");
- }
- }
- }
-
- function renderRecentList(files) {
- const listEl = document.getElementById("recent-list");
- const emptyEl = document.getElementById("recent-empty");
- if (!listEl) return;
- listEl.innerHTML = "";
-
- if (!files || files.length === 0) {
- if (emptyEl) {
- emptyEl.classList.remove("hidden");
- safeCreateIcons();
- }
- return;
- }
- if (emptyEl) emptyEl.classList.add("hidden");
-
- files.forEach((f) => {
- const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path });
-
- // Header row: time + vault badge
- const header = el("div", { class: "recent-item-header" });
- const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]);
- const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]);
- header.appendChild(timeSpan);
- header.appendChild(badge);
- item.appendChild(header);
-
- // Title
- const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]);
- item.appendChild(titleEl);
-
- // Path breadcrumb
- const pathParts = f.path.split("/");
- if (pathParts.length > 1) {
- const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]);
- item.appendChild(pathEl);
- }
-
- // Preview
- if (f.preview) {
- const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]);
- item.appendChild(previewEl);
- }
-
- // Tags
- if (f.tags && f.tags.length > 0) {
- const tagsEl = el("div", { class: "recent-item-tags" });
- f.tags.forEach((t) => {
- tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)]));
- });
- item.appendChild(tagsEl);
- }
-
- // Click handler
- item.addEventListener("click", () => {
- openFile(f.vault, f.path);
- closeMobileSidebar();
- });
-
- listEl.appendChild(item);
- });
- safeCreateIcons();
- }
-
- function _humanizeDelta(mtime) {
- const delta = Date.now() / 1000 - mtime;
- if (delta < 60) return "à l'instant";
- if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
- if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
- if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
- return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
- }
-
- function _refreshRecentTimestamps() {
- if (activeSidebarTab !== "recent" || !_recentFilesCache.length) return;
- const items = document.querySelectorAll(".recent-item");
- items.forEach((item, i) => {
- if (i < _recentFilesCache.length) {
- const timeSpan = item.querySelector(".recent-time");
- if (timeSpan) {
- // keep the icon, update text
- const textNode = timeSpan.lastChild;
- if (textNode && textNode.nodeType === Node.TEXT_NODE) {
- textNode.textContent = _humanizeDelta(_recentFilesCache[i].mtime);
- }
- }
- }
- });
- }
-
- function _populateRecentVaultFilter() {
- const select = document.getElementById("recent-vault-filter");
- if (!select) return;
- // keep first option "Tous les vaults"
- while (select.options.length > 1) select.remove(1);
- allVaults.forEach((v) => {
- const opt = document.createElement("option");
- opt.value = v.name;
- opt.textContent = v.name;
- select.appendChild(opt);
- });
- syncVaultSelectors();
- }
-
- function initRecentTab() {
- const select = document.getElementById("recent-vault-filter");
- if (select) {
- select.addEventListener("change", async () => {
- const val = select.value || "all";
- await setSelectedVaultContext(val, { focusVault: val !== "all" });
- });
- }
- // Periodic timestamp refresh (every 60s)
- _recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
- }
-
- // ---------------------------------------------------------------------------
- // Sidebar tabs
- // ---------------------------------------------------------------------------
- function initSidebarTabs() {
- document.querySelectorAll(".sidebar-tab").forEach((tab) => {
- tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
- });
- }
-
- function switchSidebarTab(tab) {
- activeSidebarTab = tab;
- document.querySelectorAll(".sidebar-tab").forEach((btn) => {
- const isActive = btn.dataset.tab === tab;
- btn.classList.toggle("active", isActive);
- btn.setAttribute("aria-selected", isActive ? "true" : "false");
- });
- document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
- const isActive = panel.id === `sidebar-panel-${tab}`;
- panel.classList.toggle("active", isActive);
- });
- const filterInput = document.getElementById("sidebar-filter-input");
- if (filterInput) {
- const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
- filterInput.placeholder = placeholders[tab] || "";
- }
- const query = filterInput ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : "";
- if (query) {
- if (tab === "vaults") performTreeSearch(query);
- else if (tab === "tags") filterTagCloud(query);
- }
- // Auto-load recent files when switching to the recent tab
- if (tab === "recent") {
- _populateRecentVaultFilter();
- const vaultFilter = document.getElementById("recent-vault-filter");
- loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
- }
- }
-
- function initHelpModal() {
- const openBtn = document.getElementById("help-open-btn");
- const closeBtn = document.getElementById("help-close");
- const modal = document.getElementById("help-modal");
- if (!openBtn || !closeBtn || !modal) return;
-
- openBtn.addEventListener("click", () => {
- modal.classList.add("active");
- closeHeaderMenu();
- safeCreateIcons();
- initHelpNavigation();
- });
-
- closeBtn.addEventListener("click", closeHelpModal);
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- closeHelpModal();
- }
- });
-
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeHelpModal();
- }
- });
- }
-
- function initEditorPocBtn() {
- var btn = document.getElementById("editor-poc-btn");
- if (!btn) return;
- btn.addEventListener("click", function() {
- closeHeaderMenu();
- window.open("/editor-poc", "_blank");
- });
- }
-
- function initHelpNavigation() {
- const helpContent = document.querySelector(".help-content");
- const helpBody = document.getElementById("help-body");
- const navLinks = document.querySelectorAll(".help-nav-link");
-
- if (!helpContent || !helpBody || !navLinks.length) return;
-
- // Handle nav link clicks
- navLinks.forEach((link) => {
- link.addEventListener("click", (e) => {
- e.preventDefault();
- const targetId = link.getAttribute("href").substring(1);
- const targetSection = document.getElementById(targetId);
- if (targetSection) {
- targetSection.scrollIntoView({ behavior: "smooth", block: "start" });
- }
- });
- });
-
- // Scroll spy - update active nav link based on scroll position
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting) {
- const id = entry.target.getAttribute("id");
- navLinks.forEach((link) => {
- if (link.getAttribute("href") === `#${id}`) {
- navLinks.forEach((l) => l.classList.remove("active"));
- link.classList.add("active");
- }
- });
- }
- });
- },
- {
- root: helpBody,
- rootMargin: "-20% 0px -70% 0px",
- threshold: 0,
- },
- );
-
- // Observe all sections
- document.querySelectorAll(".help-section").forEach((section) => {
- observer.observe(section);
- });
- }
-
- function closeHelpModal() {
- const modal = document.getElementById("help-modal");
- if (modal) modal.classList.remove("active");
- }
-
- function initConfigModal() {
- const openBtn = document.getElementById("config-open-btn");
- const closeBtn = document.getElementById("config-close");
- const modal = document.getElementById("config-modal");
- const addBtn = document.getElementById("config-add-btn");
- const patternInput = document.getElementById("config-pattern-input");
-
- if (!openBtn || !closeBtn || !modal) return;
-
- openBtn.addEventListener("click", async () => {
- modal.classList.add("active");
- closeHeaderMenu();
- renderConfigFilters();
- loadConfigFields();
- loadDiagnostics();
- loadAbout();
- await loadHiddenFilesSettings();
- loadWebhooksUI();
- loadSharesUI();
- safeCreateIcons();
- });
-
- closeBtn.addEventListener("click", closeConfigModal);
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- closeConfigModal();
- }
- });
-
- addBtn.addEventListener("click", addConfigFilter);
- patternInput.addEventListener("keypress", (e) => {
- if (e.key === "Enter") {
- addConfigFilter();
- }
- });
-
- patternInput.addEventListener("input", updateRegexPreview);
-
- // Frontend config fields — save to localStorage on change
- ["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => {
- const input = document.getElementById(id);
- if (input) input.addEventListener("change", saveFrontendConfig);
- });
-
- // Backend save button
- const saveBtn = document.getElementById("cfg-save-backend");
- if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig);
-
- // Force reindex
- const reindexBtn = document.getElementById("cfg-reindex");
- if (reindexBtn) reindexBtn.addEventListener("click", forceReindex);
-
- // Reset defaults
- const resetBtn = document.getElementById("cfg-reset-defaults");
- if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults);
-
- // Refresh diagnostics
- const diagBtn = document.getElementById("cfg-refresh-diag");
- if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics);
-
- // Hidden files configuration
- const saveHiddenBtn = document.getElementById("cfg-save-hidden-files");
- if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings);
-
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeConfigModal();
- }
- });
-
- // Load saved frontend config on startup
- applyFrontendConfig();
- }
-
- function closeConfigModal() {
- const modal = document.getElementById("config-modal");
- if (modal) modal.classList.remove("active");
- }
-
- // --- Config field helpers ---
- const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
-
- function _getFrontendConfig() {
- try {
- return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
- } catch {
- return {};
- }
- }
-
- function applyFrontendConfig() {
- const cfg = _getFrontendConfig();
- if (cfg.debounce_ms) {
- /* applied dynamically in debounce setTimeout */
- }
- if (cfg.results_per_page) {
- /* used as ADVANCED_SEARCH_LIMIT override */
- }
- if (cfg.min_query_length) {
- /* used as MIN_SEARCH_LENGTH override */
- }
- if (cfg.search_timeout_ms) {
- /* used as SEARCH_TIMEOUT_MS override */
- }
- }
-
- function _getEffective(key, fallback) {
- const cfg = _getFrontendConfig();
- return cfg[key] !== undefined ? cfg[key] : fallback;
- }
-
- async function loadConfigFields() {
- // Frontend fields from localStorage
- const cfg = _getFrontendConfig();
- _setField("cfg-debounce", cfg.debounce_ms || 300);
- _setField("cfg-results-per-page", cfg.results_per_page || 50);
- _setField("cfg-min-query", cfg.min_query_length || 2);
- _setField("cfg-timeout", cfg.search_timeout_ms || 30000);
-
- // Backend fields from API
- try {
- const data = await api("/api/config");
- _setField("cfg-workers", data.search_workers);
- _setField("cfg-max-content", data.max_content_size);
- _setField("cfg-title-boost", data.title_boost);
- _setField("cfg-tag-boost", data.tag_boost);
- _setField("cfg-prefix-exp", data.prefix_max_expansions);
- _setField("cfg-recent-limit", data.recent_files_limit || 20);
- // Watcher config
- _setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
- _setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
- _setField("cfg-watcher-interval", data.watcher_polling_interval || 5);
- _setField("cfg-watcher-debounce", data.watcher_debounce || 2);
- } catch (err) {
- console.error("Failed to load backend config:", err);
- }
- }
-
- function _setField(id, value) {
- const el = document.getElementById(id);
- if (el && value !== undefined) el.value = value;
- }
-
- function _setCheckbox(id, checked) {
- const el = document.getElementById(id);
- if (el) el.checked = !!checked;
- }
-
- function _getCheckbox(id) {
- const el = document.getElementById(id);
- return el ? el.checked : false;
- }
-
- function _getFieldNum(id, fallback) {
- const el = document.getElementById(id);
- if (!el) return fallback;
- const v = parseFloat(el.value);
- return isNaN(v) ? fallback : v;
- }
-
- function saveFrontendConfig() {
- const cfg = {
- debounce_ms: _getFieldNum("cfg-debounce", 300),
- results_per_page: _getFieldNum("cfg-results-per-page", 50),
- min_query_length: _getFieldNum("cfg-min-query", 2),
- search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
- };
- localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
- showToast("Paramètres client sauvegardés", "success");
- }
-
- async function saveBackendConfig() {
- const body = {
- search_workers: _getFieldNum("cfg-workers", 2),
- max_content_size: _getFieldNum("cfg-max-content", 100000),
- title_boost: _getFieldNum("cfg-title-boost", 3.0),
- tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
- prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
- recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
- watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
- watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
- watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
- watcher_debounce: _getFieldNum("cfg-watcher-debounce", 2.0),
- };
- try {
- const res = await fetch("/api/config", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- if (res.ok) {
- showToast("Configuration backend sauvegardée", "success");
- } else {
- const errorData = await res.json().catch(() => ({}));
- showToast(errorData.detail || "Erreur de sauvegarde", "error");
- }
- } catch (err) {
- console.error("Failed to save backend config:", err);
- showToast("Erreur de sauvegarde", "error");
- }
- }
-
- async function forceReindex() {
- const btn = document.getElementById("cfg-reindex");
- if (btn) {
- btn.disabled = true;
- btn.textContent = "Réindexation...";
- }
- try {
- await api("/api/index/reload");
- showToast("Réindexation terminée", "success");
- loadDiagnostics();
- await Promise.all([loadVaults(), loadTags()]);
- } catch (err) {
- console.error("Reindex error:", err);
- showToast("Erreur de réindexation", "error");
- } finally {
- if (btn) {
- btn.disabled = false;
- btn.textContent = "Forcer réindexation";
- }
- }
- }
-
- async function resetConfigDefaults() {
- // Reset frontend
- localStorage.removeItem(_FRONTEND_CONFIG_KEY);
- // Reset backend
- try {
- await fetch("/api/config", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- search_workers: 2,
- debounce_ms: 300,
- results_per_page: 50,
- min_query_length: 2,
- search_timeout_ms: 30000,
- max_content_size: 100000,
- title_boost: 3.0,
- path_boost: 1.5,
- tag_boost: 2.0,
- prefix_max_expansions: 50,
- snippet_context_chars: 120,
- max_snippet_highlights: 5,
- }),
- });
- } catch (err) {
- console.error("Reset config error:", err);
- }
- loadConfigFields();
- showToast("Configuration réinitialisée", "success");
- }
-
- async function loadDiagnostics() {
- const container = document.getElementById("config-diagnostics");
- if (!container) return;
- container.innerHTML = 'Chargement...
';
- try {
- const data = await api("/api/diagnostics");
- renderDiagnostics(container, data);
- } catch (err) {
- container.innerHTML = 'Erreur de chargement
';
- }
- }
-
- function renderDiagnostics(container, data) {
- container.innerHTML = "";
- const sections = [
- {
- title: "Index",
- rows: [
- ["Fichiers indexés", data.index.total_files],
- ["Tags uniques", data.index.total_tags],
- ["Vaults", Object.keys(data.index.vaults).join(", ")],
- ],
- },
- {
- title: "Index inversé",
- rows: [
- ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
- ["Postings total", data.inverted_index.total_postings.toLocaleString()],
- ["Documents", data.inverted_index.documents],
- ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
- ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
- ],
- },
- {
- title: "Moteur de recherche",
- rows: [
- ["Executor actif", data.search_executor.active ? "Oui" : "Non"],
- ["Workers max", data.search_executor.max_workers],
- ],
- },
- ];
- sections.forEach((section) => {
- const div = document.createElement("div");
- div.className = "config-diag-section";
- const title = document.createElement("div");
- title.className = "config-diag-section-title";
- title.textContent = section.title;
- div.appendChild(title);
- section.rows.forEach(([label, value]) => {
- const row = document.createElement("div");
- row.className = "config-diag-row";
- row.innerHTML = `${label} ${value} `;
- div.appendChild(row);
- });
- container.appendChild(div);
- });
- }
-
- // --- About Section ---
-
- function loadAbout() {
- const container = document.getElementById("config-about");
- if (!container) return;
-
- // Fetch health info for version
- api("/api/health").then((health) => {
- container.innerHTML = "";
-
- const sections = [
- {
- title: "Application",
- rows: [
- ["Nom", "ObsiGate"],
- ["Version", APP_VERSION],
- ["Version API", health.version || "—"],
- ["Statut", health.status || "—"],
- ],
- },
- {
- title: "Environnement",
- rows: [
- ["Vaults configurés", health.vaults || "—"],
- ["Fichiers indexés", health.total_files || "—"],
- ["Navigateur", navigator.userAgent.split(" ").pop()],
- ["Plateforme", navigator.platform || "—"],
- ["Langue", navigator.language || "—"],
- ],
- },
- {
- title: "Composants",
- rows: [
- ["Backend", "FastAPI (Python)"],
- ["Rendu Markdown", "mistune"],
- ["Surveillance fichiers", "watchdog"],
- ["Frontend", "Vanilla JavaScript"],
- ["Icônes", "Lucide Icons"],
- ["Coloration syntaxe", "highlight.js"],
- ["Éditeur", "CodeMirror 6"],
- ],
- },
- ];
-
- sections.forEach((section) => {
- const div = document.createElement("div");
- div.className = "config-diag-section";
- const title = document.createElement("div");
- title.className = "config-diag-section-title";
- title.textContent = section.title;
- div.appendChild(title);
- section.rows.forEach(([label, value]) => {
- const row = document.createElement("div");
- row.className = "config-diag-row";
- row.innerHTML = `${label} ${value} `;
- div.appendChild(row);
- });
- container.appendChild(div);
- });
- }).catch(() => {
- container.innerHTML = 'Erreur de chargement
';
- });
- }
-
- // --- Hidden Files Configuration ---
-
- async function loadHiddenFilesSettings() {
- const container = document.getElementById("hidden-files-vault-list");
- if (!container) return;
-
- container.innerHTML = 'Chargement...
';
-
- try {
- const settings = await api("/api/vaults/settings/all");
- renderHiddenFilesSettings(container, settings);
- } catch (err) {
- console.error("Failed to load hidden files settings:", err);
- container.innerHTML = 'Erreur de chargement
';
- }
- }
-
- function renderHiddenFilesSettings(container, allSettings) {
- container.innerHTML = "";
-
- if (!allVaults || allVaults.length === 0) {
- container.innerHTML = 'Aucun vault configuré
';
- return;
- }
-
- allVaults.forEach((vault) => {
- const settings = allSettings[vault.name] || { hideHiddenFiles: false };
-
- const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name });
-
- // Vault header
- const header = el("div", { class: "hidden-files-vault-header" }, [el("h3", {}, [document.createTextNode(vault.name)]), el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")])]);
-
- // Hide hidden files toggle
- const toggleRow = el("div", { class: "config-row" }, [
- el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]),
- el("label", { class: "config-toggle" }, [
- el("input", {
- type: "checkbox",
- id: `hide-hidden-${vault.name}`,
- "data-vault": vault.name,
- checked: settings.hideHiddenFiles ? "true" : false,
- }),
- el("span", { class: "config-toggle-slider" }),
- ]),
- el("span", { class: "config-hint" }, [document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)")]),
- ]);
-
- vaultCard.appendChild(header);
- vaultCard.appendChild(toggleRow);
-
- container.appendChild(vaultCard);
- });
- }
-
- async function saveHiddenFilesSettings() {
- const btn = document.getElementById("cfg-save-hidden-files");
- if (btn) {
- btn.disabled = true;
- btn.textContent = "Sauvegarde...";
- }
-
- try {
- const vaultCards = document.querySelectorAll(".hidden-files-vault-card");
- const promises = [];
-
- vaultCards.forEach((card) => {
- const vaultName = card.dataset.vault;
- const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false;
-
- const settings = {
- hideHiddenFiles,
- };
-
- promises.push(
- api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(settings),
- }),
- );
- });
-
- await Promise.all(promises);
-
- // Reload vault settings to update the cache
- await loadVaultSettings();
-
- showToast("✓ Paramètres sauvegardés", "success");
-
- // Refresh the UI to apply the filter
- await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
- } catch (err) {
- console.error("Failed to save hidden files settings:", err);
- const errorMsg = err.message || "Erreur inconnue";
- showToast(`Erreur: ${errorMsg}`, "error");
- } finally {
- if (btn) {
- btn.disabled = false;
- btn.textContent = "💾 Sauvegarder";
- }
- }
- }
-
- // ── Webhooks UI ──
- async function loadWebhooksUI() {
- const list = document.getElementById("webhooks-list");
- if (!list) return;
- try {
- const webhooks = await api("/api/webhooks");
- renderWebhooksUI(webhooks);
- } catch { list.innerHTML = 'Admin uniquement
'; }
- }
- function renderWebhooksUI(webhooks) {
- const list = document.getElementById("webhooks-list");
- if (!list) return;
- if (!webhooks.length) { list.innerHTML = 'Aucun webhook configuré.
'; return; }
- list.innerHTML = webhooks.map(w => `
-
- ${escapeHtml(w.name)}
- ${escapeHtml(w.url)}
- ${(w.events||[]).join(", ")}
- ✕
-
- `).join("");
- list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => {
- await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" });
- loadWebhooksUI();
- }));
- }
- document.addEventListener("click", function(e) {
- if (e.target.id === "webhook-add-btn") {
- const name = document.getElementById("webhook-name-input").value.trim();
- const url = document.getElementById("webhook-url-input").value.trim();
- if (!url) { showToast("URL requise", "error"); return; }
- api("/api/webhooks", { method: "POST", body: JSON.stringify({ name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] }) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; }).catch(err => showToast(err.message, "error"));
- }
- });
-
- // ── Shares UI ──
- async function loadSharesUI() {
- const list = document.getElementById("shares-list");
- if (!list) return;
- try {
- const shares = await api("/api/shares");
- renderSharesUI(shares);
- } catch { list.innerHTML = 'Chargement...
'; }
- }
- function renderSharesUI(shares) {
- const list = document.getElementById("shares-list");
- if (!list) return;
- if (!shares.length) { list.innerHTML = 'Aucun partage actif.
'; return; }
- list.innerHTML = shares.map(s => `
-
-
${escapeHtml(s.vault)}/${escapeHtml(s.path)}
-
${s.url}
-
${s.access_count} vue(s)${s.expires_at ? ' · Expire' : ''}
-
Révoquer
-
- `).join("");
- list.querySelectorAll(".share-revoke").forEach(b => b.addEventListener("click", async () => {
- await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
- loadSharesUI();
- }));
- }
-
- // ── Share Dialog (professional) ──
- async function openShareDialog(vault, path) {
- // First check if already shared
- let existingShare = null;
- try {
- const shares = await api("/api/shares");
- existingShare = shares.find(s => s.vault === vault && s.path === path);
- } catch (e) { /* ignore */ }
-
- const div = document.createElement("div");
- div.className = "share-dialog-overlay";
-
- const renderContent = () => {
- if (existingShare) {
- const url = window.location.origin + existingShare.url;
- const expiresInfo = existingShare.expires_at
- ? `Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}
`
- : 'Sans expiration
';
- div.innerHTML = `
-
-
📤 Document partagé
-
${escapeHtml(vault)}/${escapeHtml(path)}
- ${expiresInfo}
-
${existingShare.access_count} vue(s)
-
-
- 📋 Copier le lien
- 🗑 Révoquer
- Fermer
-
-
`;
- div.querySelector(".share-copy-btn").addEventListener("click", async () => {
- try {
- await navigator.clipboard.writeText(url);
- } catch (e) {
- // Fallback for non-HTTPS contexts
- const ta = document.createElement("textarea");
- ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px";
- document.body.appendChild(ta); ta.select(); document.execCommand("copy");
- document.body.removeChild(ta);
- }
- showToast("Lien copié !", "success");
- div.remove();
- });
- div.querySelector(".share-revoke-btn").addEventListener("click", async () => {
- try {
- await api(`/api/share/${existingShare.id}`, { method: "DELETE" });
- showToast("Partage révoqué", "success");
- existingShare = null;
- renderContent();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- } else {
- div.innerHTML = `
-
-
📤 Partager ce document
-
${escapeHtml(vault)}/${escapeHtml(path)}
-
Ce lien sera accessible publiquement, sans authentification .
-
- Expiration :
-
- Jamais
- 1 heure
- 24 heures
- 7 jours
- 30 jours
-
-
-
- 🔗 Créer le lien
- Fermer
-
-
`;
- div.querySelector(".share-create-btn").addEventListener("click", async () => {
- try {
- const expiry = document.getElementById("share-expiry")?.value;
- const share = await api(`/api/share/${encodeURIComponent(vault)}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }),
- });
- existingShare = share;
- renderContent();
- showToast("Lien créé !", "success");
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- }
- div.querySelector(".share-close-btn").addEventListener("click", () => div.remove());
- div.addEventListener("click", (e) => { if (e.target === div) div.remove(); });
- };
-
- renderContent();
- document.body.appendChild(div);
- }
-
- function renderConfigFilters() {
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
- const container = document.getElementById("config-filters-list");
-
- container.innerHTML = "";
-
- filters.forEach((filter, index) => {
- const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
- el("span", {}, [document.createTextNode(filter.pattern)]),
- el(
- "button",
- {
- class: "config-filter-toggle",
- title: filter.enabled ? "Désactiver" : "Activer",
- type: "button",
- },
- [document.createTextNode(filter.enabled ? "✓" : "○")],
- ),
- el(
- "button",
- {
- class: "config-filter-remove",
- title: "Supprimer",
- type: "button",
- },
- [document.createTextNode("×")],
- ),
- ]);
-
- const toggleBtn = badge.querySelector(".config-filter-toggle");
- const removeBtn = badge.querySelector(".config-filter-remove");
-
- toggleBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- toggleConfigFilter(index);
- });
-
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeConfigFilter(index);
- });
-
- container.appendChild(badge);
- });
- }
-
- function toggleConfigFilter(index) {
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
- if (filters[index]) {
- filters[index].enabled = !filters[index].enabled;
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
- }
- }
-
- function removeConfigFilter(index) {
- const config = TagFilterService.getConfig();
- let filters = config.tagFilters || TagFilterService.defaultFilters;
- filters = filters.filter((_, i) => i !== index);
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
- }
-
- function addConfigFilter() {
- const input = document.getElementById("config-pattern-input");
- const pattern = input.value.trim();
-
- if (!pattern) return;
-
- const regex = TagFilterService.patternToRegex(pattern);
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
-
- const newFilter = { pattern, regex, enabled: true };
- filters.push(newFilter);
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
-
- input.value = "";
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
- updateRegexPreview();
- }
-
- function updateRegexPreview() {
- const input = document.getElementById("config-pattern-input");
- const preview = document.getElementById("config-regex-preview");
- const code = document.getElementById("config-regex-code");
- const pattern = input.value.trim();
-
- if (pattern) {
- const regex = TagFilterService.patternToRegex(pattern);
- code.textContent = `^${regex}$`;
- preview.style.display = "block";
- } else {
- preview.style.display = "none";
- }
- }
-
- // ---------------------------------------------------------------------------
- // Search (enhanced with autocomplete, keyboard nav, global shortcuts)
- // ---------------------------------------------------------------------------
- // ── Search toggle state ──
-
- function initSearch() {
- const input = document.getElementById("search-input");
- if (!input) return;
- const caseBtn = document.getElementById("rb-case");
- const wordBtn = document.getElementById("rb-word");
- const regexBtn = document.getElementById("rb-regex");
- const filterBtn = document.getElementById("search-filter-btn");
- const clearBtn = document.getElementById("search-clear-btn");
- const filterRow = document.getElementById("search-filter-row");
- const prevBtn = document.getElementById("rb-prev");
- const nextBtn = document.getElementById("rb-next");
- const countEl = document.getElementById("rb-count");
- const resultsBar = document.getElementById("results-bar");
-
- function _showResultsBar() { if (resultsBar) resultsBar.style.display = 'flex'; }
- function _hideResultsBar() { if (resultsBar) resultsBar.style.display = 'none'; }
-
- function _updateToggleUI() {
- if (caseBtn) caseBtn.classList.toggle("active", searchCaseSensitive);
- if (wordBtn) wordBtn.classList.toggle("active", searchWholeWord);
- if (regexBtn) regexBtn.classList.toggle("active", searchRegex);
- if (filterBtn) filterBtn.classList.toggle("active", searchFilterVisible);
- }
-
- // Toggle buttons
- if (caseBtn) caseBtn.addEventListener("click", () => { searchCaseSensitive = !searchCaseSensitive; _updateToggleUI(); _research(); });
- if (wordBtn) wordBtn.addEventListener("click", () => { searchWholeWord = !searchWholeWord; _updateToggleUI(); _research(); });
- if (regexBtn) regexBtn.addEventListener("click", () => { searchRegex = !searchRegex; _updateToggleUI(); _research(); });
- if (filterBtn) filterBtn.addEventListener("click", () => { searchFilterVisible = !searchFilterVisible; if (filterRow) filterRow.style.display = searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
-
- // ── Result navigation (up/down arrows + Enter) ──
- let _searchResultIdx = -1;
- let _searchResultItems = [];
-
- function _updateResultHighlight() {
- _searchResultItems.forEach((el, i) => {
- el.classList.toggle("search-result-active", i === _searchResultIdx);
- });
- if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) {
- _searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" });
- }
- const countEl = document.getElementById("rb-count");
- if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`;
- }
-
- function _refreshResultItems() {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- _searchResultIdx = _searchResultItems.length > 0 ? 0 : -1;
- _updateResultHighlight();
- if (_searchResultItems.length > 0) _showResultsBar(); else _hideResultsBar();
- }
-
- window.navigateSearchResults = function(delta) {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- if (_searchResultItems.length === 0) return;
- _searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta));
- _updateResultHighlight();
- };
-
- if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1));
- if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1));
-
- function _research() {
- const q = input.value.trim();
- if (q.length >= _getEffective("min_query_length", 2)) {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
- advancedSearchOffset = 0;
- performAdvancedSearch(q, vault, tagFilter);
- }, _getEffective("debounce_ms", 300));
- }
- }
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- if (e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
- else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
- else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
- else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
- }
- });
-
- // Initialize sub-controllers
- AutocompleteDropdown.init();
- SearchChips.init();
-
- // Initially hide clear button
- if (clearBtn) clearBtn.style.display = "none";
-
- // --- Input handler: autocomplete dropdown only (search triggered on Enter or selection) ---
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
-
- // Show autocomplete dropdown while typing
- AutocompleteDropdown.populate(input.value, input.selectionStart);
-
- // Clear results when input becomes empty
- if (!hasText) {
- SearchChips.clear();
- showWelcome();
- }
- });
-
- // --- Focus handler: show history dropdown ---
- input.addEventListener("focus", () => {
- if (input.value.length === 0) {
- const historyItems = SearchHistory.filter("").slice(0, 5);
- if (historyItems.length > 0) {
- AutocompleteDropdown.populate("", 0);
- }
- }
- });
-
- // --- Keyboard navigation in dropdown ---
- input.addEventListener("keydown", (e) => {
- if (AutocompleteDropdown.isVisible()) {
- if (e.key === "ArrowDown") {
- e.preventDefault();
- AutocompleteDropdown.navigateDown();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- AutocompleteDropdown.navigateUp();
- } else if (e.key === "Enter") {
- // First: check dropdown suggestions (higher priority than search results)
- if (AutocompleteDropdown.selectActive()) {
- e.preventDefault();
- return;
- }
- // Second: navigate search results if visible
- const results = document.querySelectorAll(".search-result-item");
- if (results.length > 0 && _searchResultIdx >= 0) {
- const el = results[_searchResultIdx];
- if (el) {
- const vault = el.dataset.vault;
- const path = el.dataset.path;
- if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; }
- }
- }
- // Third: execute search
- AutocompleteDropdown.hide();
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(searchTimeout);
- advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- } else if (e.key === "Escape") {
- AutocompleteDropdown.hide();
- e.stopPropagation();
- }
- } else {
- // Dropdown not visible: navigate search results or execute search
- if (e.key === "ArrowDown") {
- if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); }
- } else if (e.key === "ArrowUp") {
- if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); }
- } else if (e.key === "Enter") {
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(searchTimeout);
- advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- }
- }
- });
-
- clearBtn.addEventListener("click", () => {
- input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- searchCaseSensitive = false;
- searchWholeWord = false;
- searchRegex = false;
- _updateToggleUI();
- SearchChips.clear();
- AutocompleteDropdown.hide();
- showWelcome();
- });
-
- // --- Global keyboard shortcuts ---
- document.addEventListener("keydown", (e) => {
- // Ctrl+K or Cmd+K: focus search
- if ((e.ctrlKey || e.metaKey) && e.key === "k") {
- e.preventDefault();
- input.focus();
- input.select();
- }
- // "/" key: focus search (when not in an input/textarea)
- if (e.key === "/" && !_isInputFocused()) {
- e.preventDefault();
- input.focus();
- }
- // Escape: blur search input and close dropdown
- if (e.key === "Escape" && document.activeElement === input) {
- AutocompleteDropdown.hide();
- input.blur();
- }
- });
- }
-
- /** Check if user is focused on an input/textarea/contenteditable */
- function _isInputFocused() {
- const tag = document.activeElement?.tagName;
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
- return document.activeElement?.isContentEditable === true;
- }
-
- // --- Backward-compatible search (existing /api/search endpoint) ---
- async function performSearch(query, vaultFilter, tagFilter) {
- if (searchAbortController) searchAbortController.abort();
- searchAbortController = new AbortController();
- const searchId = ++currentSearchId;
- showLoading();
- let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
- if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
- try {
- const data = await api(url, { signal: searchAbortController.signal });
- if (searchId !== currentSearchId) return;
- renderSearchResults(data, query, tagFilter);
- } catch (err) {
- if (err.name === "AbortError") return;
- if (searchId !== currentSearchId) return;
- showWelcome();
- } finally {
- hideProgressBar();
- if (searchId === currentSearchId) searchAbortController = null;
- }
- }
-
- // --- Advanced search with TF-IDF, facets, pagination ---
- async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
- if (searchAbortController) searchAbortController.abort();
- searchAbortController = new AbortController();
- const searchId = ++currentSearchId;
- showLoading();
-
- const ofs = offset !== undefined ? offset : advancedSearchOffset;
- const sortBy = sort || advancedSearchSort;
- advancedSearchLastQuery = query;
-
- // Update chips from parsed query
- const parsed = QueryParser.parse(query);
- SearchChips.update(parsed);
-
- const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT);
- let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
- if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
- if (searchCaseSensitive) url += "&case_sensitive=true";
- if (searchWholeWord) url += "&whole_word=true";
- if (searchRegex) url += "®ex=true";
- const includeEl = document.getElementById("search-include-input");
- const excludeEl = document.getElementById("search-exclude-input");
- if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
- if (excludeEl?.value.trim()) url += `&exclude_paths=${encodeURIComponent(excludeEl.value.trim())}`;
-
- // Search timeout — abort if server takes too long
- const timeoutId = setTimeout(
- () => {
- if (searchAbortController) searchAbortController.abort();
- },
- _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS),
- );
-
- try {
- const data = await api(url, { signal: searchAbortController.signal });
- clearTimeout(timeoutId);
- if (searchId !== currentSearchId) return;
- advancedSearchTotal = data.total;
- advancedSearchOffset = ofs;
- renderAdvancedSearchResults(data, query, tagFilter);
- } catch (err) {
- clearTimeout(timeoutId);
- if (err.name === "AbortError") return;
- if (searchId !== currentSearchId) return;
- showWelcome();
- } finally {
- hideProgressBar();
- if (searchId === currentSearchId) searchAbortController = null;
- }
- }
-
- // --- Legacy search results renderer (kept for backward compat) ---
- function renderSearchResults(data, query, tagFilter) {
- const area = document.getElementById("content-area");
- area.innerHTML = "";
- const header = buildSearchResultsHeader(data, query, tagFilter);
- area.appendChild(header);
- if (data.results.length === 0) {
- area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")]));
- return;
- }
- const container = el("div", { class: "search-results" });
- data.results.forEach((r) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(r.path, r.vault)) {
- return; // Skip this result
- }
-
- const titleDiv = el("div", { class: "search-result-title" });
- if (query && query.trim()) {
- highlightSearchText(titleDiv, r.title, query, searchCaseSensitive);
- } else {
- titleDiv.textContent = r.title;
- }
- const snippetDiv = el("div", { class: "search-result-snippet" });
- if (r.snippet && r.snippet.includes("")) {
- snippetDiv.innerHTML = r.snippet;
- } else if (query && query.trim() && r.snippet) {
- highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive);
- } else {
- snippetDiv.textContent = r.snippet || "";
- }
- const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
- el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
- titleDiv,
- el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]),
- snippetDiv
- ]);
- if (r.tags && r.tags.length > 0) {
- const tagsDiv = el("div", { class: "search-result-tags" });
- r.tags.forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", (e) => {
- e.stopPropagation();
- addTagFilter(tag);
- });
- tagsDiv.appendChild(tagEl);
- }
- });
- if (tagsDiv.children.length > 0) item.appendChild(tagsDiv);
- }
- item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); });
- container.appendChild(item);
- });
- area.appendChild(container);
- }
-
- // --- Advanced search results renderer (facets, highlighted snippets, pagination, sort) ---
- function renderAdvancedSearchResults(data, query, tagFilter) {
- const area = document.getElementById("content-area");
- area.innerHTML = "";
-
- // Update match counter + show floating results bar
- var rb = document.getElementById("results-bar");
- var countEl = document.getElementById("rb-count");
- if (countEl) countEl.textContent = `${data.total > 0 ? "1" : "0"}/${data.total}`;
- if (rb) rb.style.display = 'flex';
-
- // Header with result count and sort controls
- const header = el("div", { class: "search-results-header" });
- const summaryText = el("span", { class: "search-results-summary-text" });
- const parsed = QueryParser.parse(query);
- const freeText = parsed.freeText;
-
- if (freeText && tagFilter) {
- summaryText.textContent = `${data.total} résultat(s) pour "${freeText}" avec filtres`;
- } else if (freeText) {
- summaryText.textContent = `${data.total} résultat(s) pour "${freeText}"`;
- } else if (parsed.tags.length > 0 || tagFilter) {
- summaryText.textContent = `${data.total} fichier(s) avec filtres`;
- } else {
- summaryText.textContent = `${data.total} résultat(s)`;
- }
- if (data.query_time_ms !== undefined && data.query_time_ms > 0) {
- const timeBadge = el("span", { class: "search-time-badge" });
- timeBadge.textContent = `(${data.query_time_ms} ms)`;
- summaryText.appendChild(timeBadge);
- }
- header.appendChild(summaryText);
-
- // Active filter badges
- const filtersRow = el("div", { class: "search-filters-row" });
- if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
- if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
- if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
- const inclEl = document.getElementById("search-include-input");
- const exclEl = document.getElementById("search-exclude-input");
- if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())]));
- if (exclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("excl: " + exclEl.value.trim())]));
- if (filtersRow.children.length > 0) header.appendChild(filtersRow);
-
- // Sort controls
- const sortDiv = el("div", { class: "search-sort" });
- const btnRelevance = el("button", { class: "search-sort__btn" + (advancedSearchSort === "relevance" ? " active" : ""), type: "button" });
- btnRelevance.textContent = "Pertinence";
- btnRelevance.addEventListener("click", () => {
- advancedSearchSort = "relevance";
- advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, 0, "relevance");
- });
- const btnDate = el("button", { class: "search-sort__btn" + (advancedSearchSort === "modified" ? " active" : ""), type: "button" });
- btnDate.textContent = "Date";
- btnDate.addEventListener("click", () => {
- advancedSearchSort = "modified";
- advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, 0, "modified");
- });
- sortDiv.appendChild(btnRelevance);
- sortDiv.appendChild(btnDate);
- header.appendChild(sortDiv);
-
- // Save search button
- const saveBtn = el("button", { class: "search-save-btn", type: "button", title: "Sauvegarder cette recherche" });
- saveBtn.innerHTML = ' Sauver';
- saveBtn.addEventListener("click", async () => {
- const inclEl = document.getElementById("search-include-input");
- const exclEl = document.getElementById("search-exclude-input");
- try {
- await api("/api/saved-searches", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- query: query,
- vault: document.getElementById("vault-filter")?.value || "all",
- case_sensitive: searchCaseSensitive,
- whole_word: searchWholeWord,
- regex: searchRegex,
- include_paths: inclEl?.value || "",
- exclude_paths: exclEl?.value || "",
- }),
- });
- showToast("Recherche sauvegardée", "success");
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- header.appendChild(saveBtn);
- area.appendChild(header);
-
- // Active sidebar tag chips
- if (selectedTags.length > 0) {
- const activeTags = el("div", { class: "search-results-active-tags" });
- selectedTags.forEach((tag) => {
- const removeBtn = el(
- "button",
- {
- class: "search-results-active-tag-remove",
- title: `Retirer ${tag} du filtre`,
- },
- [document.createTextNode("×")],
- );
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeTagFilter(tag);
- });
- const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
- activeTags.appendChild(chip);
- });
- area.appendChild(activeTags);
- }
-
- // Facets panel
- if (data.facets && (Object.keys(data.facets.tags || {}).length > 0 || Object.keys(data.facets.vaults || {}).length > 0)) {
- const facetsDiv = el("div", { class: "search-facets" });
-
- // Vault facets
- const vaultFacets = data.facets.vaults || {};
- if (Object.keys(vaultFacets).length > 1) {
- const group = el("div", { class: "search-facets__group" });
- const label = el("span", { class: "search-facets__label" });
- label.textContent = "Vaults";
- group.appendChild(label);
- for (const [vaultName, count] of Object.entries(vaultFacets)) {
- const item = el("span", { class: "search-facets__item" });
- item.innerHTML = `${vaultName} ${count} `;
- item.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- // Add vault: operator
- const current = input.value.replace(/vault:\S+\s*/gi, "").trim();
- input.value = current + " vault:" + vaultName;
- _triggerAdvancedSearch(input.value);
- });
- group.appendChild(item);
- }
- facetsDiv.appendChild(group);
- }
-
- // Tag facets
- const tagFacets = data.facets.tags || {};
- if (Object.keys(tagFacets).length > 0) {
- const group = el("div", { class: "search-facets__group" });
- const label = el("span", { class: "search-facets__label" });
- label.textContent = "Tags";
- group.appendChild(label);
- const entries = Object.entries(tagFacets).slice(0, 12);
- for (const [tagName, count] of entries) {
- const item = el("span", { class: "search-facets__item" });
- item.innerHTML = `#${tagName} ${count} `;
- item.addEventListener("click", () => {
- addTagFilter(tagName);
- });
- group.appendChild(item);
- }
- facetsDiv.appendChild(group);
- }
-
- area.appendChild(facetsDiv);
- }
-
- // Empty state
- if (data.results.length === 0) {
- area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")]));
- return;
- }
-
- // Results list
- const container = el("div", { class: "search-results" });
- data.results.forEach((r) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(r.path, r.vault)) {
- return; // Skip this result
- }
-
- const titleDiv = el("div", { class: "search-result-title" });
- if (freeText) {
- highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive);
- } else {
- titleDiv.textContent = r.title;
- }
-
- // Snippet — use HTML from backend (already has tags)
- const snippetDiv = el("div", { class: "search-result-snippet search-result__snippet" });
- if (r.snippet && r.snippet.includes("")) {
- snippetDiv.innerHTML = r.snippet;
- } else if (freeText && r.snippet) {
- highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive);
- } else {
- snippetDiv.textContent = r.snippet || "";
- }
-
- // Score badge
- const scoreEl = el("span", { class: "search-result-score", style: "font-size:0.7rem;color:var(--text-muted);margin-left:8px" });
- scoreEl.textContent = `score: ${r.score}`;
-
- const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]);
-
- const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
- el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
- titleDiv, vaultPath, snippetDiv
- ]);
-
- if (r.tags && r.tags.length > 0) {
- const tagsDiv = el("div", { class: "search-result-tags" });
- r.tags.forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", (e) => {
- e.stopPropagation();
- addTagFilter(tag);
- });
- tagsDiv.appendChild(tagEl);
- }
- });
- if (tagsDiv.children.length > 0) item.appendChild(tagsDiv);
- }
-
- item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); });
- container.appendChild(item);
- });
- area.appendChild(container);
-
- // Pagination
- if (data.total > ADVANCED_SEARCH_LIMIT) {
- const paginationDiv = el("div", { class: "search-pagination" });
- const prevBtn = el("button", { class: "search-pagination__btn", type: "button" });
- prevBtn.textContent = "← Précédent";
- prevBtn.disabled = advancedSearchOffset === 0;
- prevBtn.addEventListener("click", () => {
- advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT);
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
- document.getElementById("content-area").scrollTop = 0;
- });
-
- const info = el("span", { class: "search-pagination__info" });
- const from = advancedSearchOffset + 1;
- const to = Math.min(advancedSearchOffset + ADVANCED_SEARCH_LIMIT, data.total);
- info.textContent = `${from}–${to} sur ${data.total}`;
-
- const nextBtn = el("button", { class: "search-pagination__btn", type: "button" });
- nextBtn.textContent = "Suivant →";
- nextBtn.disabled = advancedSearchOffset + ADVANCED_SEARCH_LIMIT >= data.total;
- nextBtn.addEventListener("click", () => {
- advancedSearchOffset += ADVANCED_SEARCH_LIMIT;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
- document.getElementById("content-area").scrollTop = 0;
- });
-
- paginationDiv.appendChild(prevBtn);
- paginationDiv.appendChild(info);
- paginationDiv.appendChild(nextBtn);
- area.appendChild(paginationDiv);
- }
-
- safeCreateIcons();
- // Initialize result navigation (select first result)
- setTimeout(() => { if (window.navigateSearchResults) window.navigateSearchResults(0); }, 50);
- }
-
- // ---------------------------------------------------------------------------
- // 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);
- });
- }
-
- // ---------------------------------------------------------------------------
- // Frontmatter Accent Card Builder
- // ---------------------------------------------------------------------------
-
- 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}`;
- }
-
- // Extract boolean flags
- const booleanFlags = ["publish", "favoris", "template", "task", "archive", "draft", "private"].map((key) => ({ key, value: !!frontmatter[key] }));
-
- // Toggle state
- let isOpen = true;
-
- // Build header with chevron
- const chevron = el("span", { class: "fm-chevron open" });
- chevron.innerHTML = ' ';
-
- const fmHeader = el("div", { class: "fm-header" }, [chevron, document.createTextNode("Frontmatter")]);
-
- // ZONE 1: Top strip
- const topBadges = [];
-
- // Title badge
- const title = frontmatter.titre || frontmatter.title || "";
- if (title) {
- topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)]));
- }
-
- // Status badge
- if (frontmatter.statut) {
- const statusBadge = el("span", { class: "ac-badge green" }, [el("span", { class: "ac-dot" }), document.createTextNode(frontmatter.statut)]);
- topBadges.push(statusBadge);
- }
-
- // Category badge
- if (frontmatter.catégorie || frontmatter.categorie) {
- const cat = frontmatter.catégorie || frontmatter.categorie;
- const catBadge = el("span", { class: "ac-badge blue" }, [document.createTextNode(cat)]);
- topBadges.push(catBadge);
- }
-
- // Publish badge
- if (frontmatter.publish) {
- topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("publié")]));
- }
-
- // Favoris badge
- if (frontmatter.favoris) {
- topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("favori")]));
- }
-
- const acTop = el("div", { class: "ac-top" }, topBadges);
-
- // ZONE 2: Body 2 columns
- const leftCol = el("div", { class: "ac-col" }, [
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("statut")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), el("span", { class: "ac-v muted" }, [document.createTextNode(frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(", ") : "[]")])]),
- ]);
-
- const rightCol = el("div", { class: "ac-col" }, [
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("publish")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])]),
- ]);
-
- const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]);
-
- // ZONE 3: Tags row
- const tagPills = [];
- if (frontmatter.tags && frontmatter.tags.length > 0) {
- frontmatter.tags.forEach((tag) => {
- tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)]));
- });
- }
-
- const acTagsRow = el("div", { class: "ac-tags-row" }, [el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), el("div", { class: "ac-tags-wrap" }, tagPills)]);
-
- // ZONE 4: Flags row
- const flagChips = [];
- booleanFlags.forEach((flag) => {
- const chipClass = flag.value ? "flag-chip on" : "flag-chip off";
- flagChips.push(el("span", { class: chipClass }, [el("span", { class: "flag-dot" }), document.createTextNode(flag.key)]));
- });
-
- const acFlagsRow = el("div", { class: "ac-flags-row" }, [el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), ...flagChips]);
-
- // Assemble the card
- const acCard = el("div", { class: "ac-card" }, [acTop, acBody, acTagsRow, 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");
- }
- safeCreateIcons();
- });
-
- // Wrap in section
- const fmSection = el("div", { class: "fm-section" }, [fmHeader, acCard]);
-
- return fmSection;
- }
-
- // ---------------------------------------------------------------------------
- // Helpers
- // ---------------------------------------------------------------------------
- function escapeHtml(str) {
- if (!str) return "";
- return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
- }
-
- function el(tag, attrs, children) {
- const e = document.createElement(tag);
- if (attrs) {
- Object.entries(attrs).forEach(([k, v]) => {
- // Skip boolean false for standard HTML boolean attributes to avoid setAttribute("checked", "false") bug
- if (v === false && (k === "checked" || k === "disabled" || k === "hidden" || k === "required" || k === "readonly")) {
- return;
- }
- 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.className = "badge-small";
- s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px";
- s.textContent = `(${count})`;
- return s;
- }
-
- function getContextMenuPositionFromElement(target) {
- const rect = target.getBoundingClientRect();
- return {
- x: Math.min(rect.right - 8, window.innerWidth - 16),
- y: Math.min(rect.top + rect.height / 2, window.innerHeight - 16),
- };
- }
-
- function attachTreeItemActionButton(itemEl, vault, path, type, isReadonly) {
- const button = document.createElement("button");
- button.type = "button";
- button.className = "tree-item-action-btn";
- button.setAttribute("aria-label", "Afficher le menu d’actions");
- button.setAttribute("title", "Actions");
- const iconEl = icon("more-vertical", 16);
- button.appendChild(iconEl);
- button.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- const pos = getContextMenuPositionFromElement(button);
- ContextMenuManager.show(pos.x, pos.y, vault, path, type, isReadonly);
- });
- itemEl.appendChild(button);
- // Ensure Lucide icons are rendered for the button
- setTimeout(() => {
- safeCreateIcons();
- }, 0);
- }
-
- function attachTreeItemLongPress(itemEl, getMenuData) {
- let pressTimer = null;
- let pressHandled = false;
- let startX = 0;
- let startY = 0;
- const longPressDelay = 550;
- const moveThreshold = 10;
-
- const clearPressTimer = () => {
- if (pressTimer) {
- clearTimeout(pressTimer);
- pressTimer = null;
- }
- };
-
- itemEl.addEventListener("touchstart", (e) => {
- if (!e.touches || e.touches.length !== 1) return;
- pressHandled = false;
- startX = e.touches[0].clientX;
- startY = e.touches[0].clientY;
- clearPressTimer();
- pressTimer = setTimeout(() => {
- const data = getMenuData();
- if (!data) return;
- pressHandled = true;
- ContextMenuManager.show(startX, startY, data.vault, data.path, data.type, data.isReadonly);
- }, longPressDelay);
- }, { passive: true });
-
- itemEl.addEventListener("touchmove", (e) => {
- if (!e.touches || e.touches.length !== 1) return;
- const dx = Math.abs(e.touches[0].clientX - startX);
- const dy = Math.abs(e.touches[0].clientY - startY);
- if (dx > moveThreshold || dy > moveThreshold) {
- clearPressTimer();
- }
- }, { passive: true });
-
- itemEl.addEventListener("touchend", () => {
- clearPressTimer();
- }, { passive: true });
-
- itemEl.addEventListener("touchcancel", () => {
- clearPressTimer();
- }, { passive: true });
-
- itemEl.addEventListener("click", (e) => {
- if (pressHandled) {
- e.preventDefault();
- e.stopPropagation();
- setTimeout(() => {
- pressHandled = false;
- }, 0);
- }
- }, true);
- }
-
- function getVaultIcon(vaultName, size = 16) {
- const v = allVaults.find((val) => val.name === vaultName);
- const type = v ? v.type : "VAULT";
-
- if (type === "DIR") {
- const i = icon("folder", size);
- i.style.color = "#eab308"; // yellow tint
- return i;
- } else {
- const purple = "#8b5cf6";
- const svgNS = "http://www.w3.org/2000/svg";
- const svg = document.createElementNS(svgNS, "svg");
- svg.setAttribute("xmlns", svgNS);
- svg.setAttribute("width", size);
- svg.setAttribute("height", size);
- svg.setAttribute("viewBox", "0 0 24 24");
- svg.setAttribute("fill", "none");
- svg.setAttribute("stroke", purple);
- svg.setAttribute("stroke-width", "2");
- svg.setAttribute("stroke-linecap", "round");
- svg.setAttribute("stroke-linejoin", "round");
- svg.classList.add("icon");
-
- const path1 = document.createElementNS(svgNS, "path");
- path1.setAttribute("d", "M6 3h12l4 6-10 12L2 9z");
- const path2 = document.createElementNS(svgNS, "path");
- path2.setAttribute("d", "M11 3 8 9l4 12");
- const path3 = document.createElementNS(svgNS, "path");
- path3.setAttribute("d", "M12 21l4-12-3-6");
- const path4 = document.createElementNS(svgNS, "path");
- path4.setAttribute("d", "M2 9h20");
-
- svg.appendChild(path1);
- svg.appendChild(path2);
- svg.appendChild(path3);
- svg.appendChild(path4);
- return svg;
- }
- }
-
- function makeBreadcrumbSpan(text, onClick) {
- const s = document.createElement("span");
- s.textContent = text;
- if (onClick) {
- s.addEventListener("click", async (event) => {
- event.preventDefault();
- if (s.dataset.busy === "true") return;
- s.dataset.busy = "true";
- s.style.pointerEvents = "none";
- try {
- await onClick(event);
- } finally {
- s.dataset.busy = "false";
- s.style.pointerEvents = "";
- }
- });
- }
- return s;
- }
-
- function appendHighlightedText(container, text, query, caseSensitive) {
- container.textContent = "";
- if (!query) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- const source = caseSensitive ? text : text.toLowerCase();
- const needle = caseSensitive ? query : query.toLowerCase();
- let start = 0;
- let index = source.indexOf(needle, start);
-
- if (index === -1) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- while (index !== -1) {
- if (index > start) {
- container.appendChild(document.createTextNode(text.slice(start, index)));
- }
- const mark = el("mark", { class: "filter-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]);
- container.appendChild(mark);
- start = index + query.length;
- index = source.indexOf(needle, start);
- }
-
- if (start < text.length) {
- container.appendChild(document.createTextNode(text.slice(start)));
- }
- }
-
- function highlightSearchText(container, text, query, caseSensitive) {
- container.textContent = "";
- if (!query || !text) {
- container.appendChild(document.createTextNode(text || ""));
- return;
- }
-
- const source = caseSensitive ? text : text.toLowerCase();
- const needle = caseSensitive ? query : query.toLowerCase();
- let start = 0;
- let index = source.indexOf(needle, start);
-
- if (index === -1) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- while (index !== -1) {
- if (index > start) {
- container.appendChild(document.createTextNode(text.slice(start, index)));
- }
- const mark = el("mark", { class: "search-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]);
- container.appendChild(mark);
- start = index + query.length;
- index = source.indexOf(needle, start);
- }
-
- if (start < text.length) {
- container.appendChild(document.createTextNode(text.slice(start)));
- }
- }
-
- function showWelcome() {
- hideProgressBar();
-
- // Restore or rebuild the dashboard with tabbed sections
- const area = document.getElementById("content-area");
- const home = document.getElementById("dashboard-home");
-
- if (area && !home) {
- area.innerHTML = `
-
-
-
-
- Statistiques
-
-
- Bookmarks
-
-
- Récents
-
-
- Partagés
-
-
-
-
-
-
-
-
-
-
-
-
Aucun bookmark
-
Épinglez des fichiers pour les retrouver ici.
-
-
-
-
-
-
-
-
-
-
-
Aucun fichier récent
-
Ouvrez un fichier pour le voir apparaître ici
-
-
-
-
-
-
-
-
-
Aucun document partagé
-
Partagez un document pour le voir apparaître ici
-
-
-
`;
-
- // Re-initialize widgets and dashboard tabs
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.init();
- }
- initDashboardTabs();
- safeCreateIcons();
- } else if (home) {
- // Dashboard already exists, show it with default tab
- home.style.display = "";
- // Reset tabs to default
- document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
- document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
- const defaultTab = document.querySelector('.dashboard-tab[data-tab="stats"]');
- const defaultPanel = document.getElementById("dashboard-panel-stats");
- if (defaultTab) defaultTab.classList.add("active");
- if (defaultPanel) defaultPanel.classList.add("active");
- }
-
- // Load all widgets (they handle missing elements gracefully)
- if (typeof DashboardStatsWidget !== "undefined") {
- DashboardStatsWidget.load();
- }
- if (typeof DashboardConflictsWidget !== "undefined") {
- DashboardConflictsWidget.load();
- }
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.load(selectedContextVault);
- }
- if (typeof DashboardBookmarkWidget !== "undefined") {
- DashboardBookmarkWidget.load(selectedContextVault);
- }
- if (typeof DashboardSharedWidget !== "undefined") {
- DashboardSharedWidget.load();
- }
-
- // Load saved searches sidebar
- loadSavedSearches();
- }
-
- async function loadSavedSearches() {
- const list = document.getElementById("saved-searches-list");
- const empty = document.getElementById("saved-searches-empty");
- if (!list) return;
- try {
- const searches = await api("/api/saved-searches");
- if (!searches.length) {
- list.innerHTML = "";
- if (empty) empty.style.display = "";
- return;
- }
- if (empty) empty.style.display = "none";
- list.innerHTML = searches.map(s => {
- const badges = [];
- if (s.case_sensitive) badges.push('Aa ');
- if (s.whole_word) badges.push('wd ');
- if (s.regex) badges.push('.* ');
- const pathFilters = [];
- if (s.include_paths) pathFilters.push(`📥 ${escapeHtml(s.include_paths)} `);
- if (s.exclude_paths) pathFilters.push(`📤 ${escapeHtml(s.exclude_paths)} `);
- const vaultStr = s.vault && s.vault !== "all" ? `📁 ${escapeHtml(s.vault)} ` : "";
- return `
-
-
${escapeHtml(s.query)}
-
- ${badges.join("")}
- ${vaultStr}
-
- ${pathFilters.length ? '
' + pathFilters.join(" ") + '
' : ""}
-
✕
-
- `}).join("");
- list.querySelectorAll(".saved-search-item").forEach(item => {
- item.addEventListener("click", (e) => {
- if (e.target.classList.contains("saved-search-delete")) return;
- const idx = Array.from(list.children).indexOf(item);
- const s = searches[idx];
- if (!s) return;
- // Apply the saved search
- const input = document.getElementById("search-input");
- if (input) input.value = s.query;
- searchCaseSensitive = s.case_sensitive || false;
- searchWholeWord = s.whole_word || false;
- searchRegex = s.regex || false;
- if (typeof _updateToggleUI === "function") _updateToggleUI();
- if (s.include_paths) {
- const incl = document.getElementById("search-include-input");
- if (incl) incl.value = s.include_paths;
- }
- if (s.exclude_paths) {
- const excl = document.getElementById("search-exclude-input");
- if (excl) excl.value = s.exclude_paths;
- }
- // Execute the search — suppress dropdown from appearing
- AutocompleteDropdown.hide();
- AutocompleteDropdown._suppressNext = true;
- const vault = s.vault || "all";
- if (input) { input.dispatchEvent(new Event("input")); }
- clearTimeout(searchTimeout);
- advancedSearchOffset = 0;
- performAdvancedSearch(s.query, vault, null);
- });
- });
- list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
- loadSavedSearches();
- }));
- safeCreateIcons();
- } catch (err) { /* silently ignore */ }
- }
-
- function showLoading() {
- const area = document.getElementById("content-area");
- area.innerHTML = `
-
-
-
Recherche en cours...
-
`;
- showProgressBar();
- }
-
- function showProgressBar() {
- const bar = document.getElementById("search-progress-bar");
- if (bar) bar.classList.add("active");
- }
-
- function hideProgressBar() {
- const bar = document.getElementById("search-progress-bar");
- if (bar) bar.classList.remove("active");
- }
-
- function goHome() {
- const searchInput = document.getElementById("search-input");
- if (searchInput) searchInput.value = "";
-
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
-
- currentVault = null;
- currentPath = null;
- showingSource = false;
- cachedRawSource = null;
-
- closeMobileSidebar();
- showWelcome();
- }
-
- // ---------------------------------------------------------------------------
- // Editor (CodeMirror)
- // ---------------------------------------------------------------------------
- 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()}`;
-
- // Fetch raw content
- const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
- const rawData = await api(rawUrl);
-
- // Clear previous editor
- bodyEl.innerHTML = "";
- 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,
- ];
-
- // Add language support based on file extension
- 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, // Using javascript for shell scripts as fallback
- 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");
- safeCreateIcons();
- }
-
- 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");
- }
- }
-
- function closeEditor() {
- const modal = document.getElementById("editor-modal");
- modal.classList.remove("active");
- if (editorView) {
- editorView.destroy();
- editorView = null;
- }
- fallbackEditorEl = null;
- // Remove forge iframe if present + refresh current file
- var forgeFrame = document.getElementById("forge-iframe");
- if (forgeFrame) {
- forgeFrame.remove();
- if (currentVault && currentPath) openFile(currentVault, currentPath);
- }
- editorVault = null;
- editorPath = null;
- }
-
- 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 = ' ';
- safeCreateIcons();
-
- const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ content }),
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || "Erreur de sauvegarde");
- }
-
- saveBtn.innerHTML = ' ';
- safeCreateIcons();
-
- setTimeout(() => {
- closeEditor();
- if (currentVault === editorVault && currentPath === editorPath) {
- openFile(currentVault, currentPath);
- }
- }, 800);
- } catch (err) {
- console.error("Save error:", err);
- alert(`Erreur: ${err.message}`);
- saveBtn.innerHTML = originalHTML;
- saveBtn.disabled = false;
- safeCreateIcons();
- }
- }
-
- async function deleteFile() {
- if (!editorVault || !editorPath) return;
-
- const deleteBtn = document.getElementById("editor-delete");
- const originalHTML = deleteBtn.innerHTML;
-
- try {
- deleteBtn.disabled = true;
- deleteBtn.innerHTML = ' ';
- safeCreateIcons();
-
- const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`, { method: "DELETE" });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || "Erreur de suppression");
- }
-
- closeEditor();
- showWelcome();
- await refreshSidebarForContext();
- await refreshTagsForContext();
- } catch (err) {
- console.error("Delete error:", err);
- alert(`Erreur: ${err.message}`);
- deleteBtn.innerHTML = originalHTML;
- deleteBtn.disabled = false;
- safeCreateIcons();
- }
- }
-
- function initEditor() {
- const cancelBtn = document.getElementById("editor-cancel");
- const deleteBtn = document.getElementById("editor-delete");
- const saveBtn = document.getElementById("editor-save");
- const modal = document.getElementById("editor-modal");
-
- cancelBtn.addEventListener("click", closeEditor);
- deleteBtn.addEventListener("click", deleteFile);
- saveBtn.addEventListener("click", saveFile);
-
- // Close on overlay click
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- closeEditor();
- }
- });
-
- // ESC to close
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeEditor();
- }
- });
-
- // Fix mouse wheel scrolling in editor
- modal.addEventListener(
- "wheel",
- (e) => {
- const editorBody = document.getElementById("editor-body");
- if (editorBody && editorBody.contains(e.target)) {
- // Let the editor handle the scroll
- return;
- }
- // Prevent modal from scrolling if not in editor area
- e.preventDefault();
- },
- { passive: false },
- );
- }
-
- // ---------------------------------------------------------------------------
- // SSE Client — IndexUpdateManager
- // ---------------------------------------------------------------------------
- const IndexUpdateManager = (() => {
- let eventSource = null;
- let reconnectTimer = null;
- let reconnectDelay = 1000;
- const MAX_RECONNECT_DELAY = 30000;
- let recentEvents = [];
- const MAX_RECENT_EVENTS = 20;
- let connectionState = "disconnected"; // disconnected | connecting | connected
-
- function connect() {
- if (eventSource) {
- eventSource.close();
- }
- connectionState = "connecting";
- _updateBadge();
-
- eventSource = new EventSource("/api/events");
-
- eventSource.addEventListener("connected", (e) => {
- connectionState = "connected";
- reconnectDelay = 1000;
- _updateBadge();
- });
-
- eventSource.addEventListener("index_updated", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_updated", data);
- _onIndexUpdated(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_reloaded", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_reloaded", data);
- _onIndexReloaded(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_added", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_added", data);
- showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_removed", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_removed", data);
- showToast(`Vault "${data.vault}" supprimé`, "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_start", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_start", data);
- connectionState = "syncing";
- _updateBadge();
- showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_progress", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_progress", data);
- connectionState = "syncing";
- _updateBadge();
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_complete", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_complete", data);
- connectionState = "connected";
- _updateBadge();
- showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.onerror = () => {
- connectionState = "disconnected";
- _updateBadge();
- eventSource.close();
- eventSource = null;
- _scheduleReconnect();
- };
- }
-
- function _scheduleReconnect() {
- if (reconnectTimer) clearTimeout(reconnectTimer);
- reconnectTimer = setTimeout(() => {
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
- connect();
- }, reconnectDelay);
- }
-
- function _addEvent(type, data) {
- recentEvents.unshift({
- type,
- data,
- timestamp: new Date().toISOString(),
- });
- if (recentEvents.length > MAX_RECENT_EVENTS) {
- recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
- }
- }
-
- async function _onIndexUpdated(data) {
- // Brief syncing state
- connectionState = "syncing";
- _updateBadge();
-
- const n = data.total_changes || 0;
- const vaults = (data.vaults || []).join(", ");
- // Toast removed: silent auto-indexing — no notification needed
-
- // Refresh sidebar and tags if affected vault matches current context
- const affectsCurrentVault = selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault);
- if (affectsCurrentVault) {
- try {
- await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
- // Refresh current file if it was updated
- if (currentVault && currentPath) {
- const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath);
- if (changed) {
- openFile(currentVault, currentPath);
- }
- }
- } catch (err) {
- console.error("Error refreshing after index update:", err);
- }
- }
-
- // Refresh recent tab if it is active
- if (activeSidebarTab === "recent") {
- const vaultFilter = document.getElementById("recent-vault-filter");
- loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
- }
-
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- async function _onIndexReloaded(data) {
- connectionState = "syncing";
- _updateBadge();
- showToast("Index complet rechargé", "info");
- try {
- await Promise.all([loadVaults(), loadTags()]);
- } catch (err) {
- console.error("Error refreshing after full reload:", err);
- }
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- function _updateBadge() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
- badge.className = "sync-badge sync-badge--" + connectionState;
- const labels = {
- disconnected: "Déconnecté",
- connecting: "Connexion...",
- connected: "Synchronisé",
- syncing: "Mise à jour...",
- };
- badge.title = labels[connectionState] || connectionState;
- }
-
- function disconnect() {
- if (eventSource) {
- eventSource.close();
- eventSource = null;
- }
- if (reconnectTimer) {
- clearTimeout(reconnectTimer);
- reconnectTimer = null;
- }
- connectionState = "disconnected";
- _updateBadge();
- }
-
- function getState() {
- return connectionState;
- }
-
- function getRecentEvents() {
- return recentEvents;
- }
-
- return { connect, disconnect, getState, getRecentEvents };
- })();
-
- function initSyncStatus() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
-
- badge.addEventListener("click", (e) => {
- e.stopPropagation();
- toggleSyncPanel();
- });
-
- IndexUpdateManager.connect();
- }
-
- function toggleSyncPanel() {
- let panel = document.getElementById("sync-panel");
- if (panel) {
- panel.remove();
- return;
- }
- // Auto reconnect if disconnected when user opens the panel
- if (IndexUpdateManager.getState() === "disconnected") {
- IndexUpdateManager.connect();
- }
- panel = document.createElement("div");
- panel.id = "sync-panel";
- panel.className = "sync-panel";
- _renderSyncPanel(panel);
- document.body.appendChild(panel);
-
- // Close on outside click
- setTimeout(() => {
- document.addEventListener("click", _closeSyncPanelOutside, { once: true });
- }, 0);
- }
-
- function _closeSyncPanelOutside(e) {
- const panel = document.getElementById("sync-panel");
- if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
- panel.remove();
- }
- }
-
- function _renderSyncPanel(panel) {
- const state = IndexUpdateManager.getState();
- const events = IndexUpdateManager.getRecentEvents();
-
- const stateLabels = {
- disconnected: "Déconnecté",
- connecting: "Connexion...",
- connected: "Connecté",
- syncing: "Synchronisation...",
- };
-
- let html = ``;
-
- if (events.length === 0) {
- html += `Aucun événement récent
`;
- } else {
- html += ``;
- events.slice(0, 10).forEach((ev) => {
- const time = new Date(ev.timestamp).toLocaleTimeString();
- const typeLabels = {
- index_updated: "Mise à jour",
- index_reloaded: "Rechargement",
- vault_added: "Vault ajouté",
- vault_removed: "Vault supprimé",
- index_start: "Démarrage index.",
- index_progress: "Vault indexé",
- index_complete: "Indexation tech.",
- };
- const label = typeLabels[ev.type] || ev.type;
- let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
- if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
- if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
- if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
- html += `
- ${label}
- ${detail}
- ${time}
-
`;
- });
- html += `
`;
- }
-
- panel.innerHTML = html;
- }
-
- // ---------------------------------------------------------------------------
- // Context Menu Manager
- // ---------------------------------------------------------------------------
-
- const ContextMenuManager = {
- _menu: null,
- _targetElement: null,
- _targetVault: null,
- _targetPath: null,
- _targetType: null,
-
- init() {
- this._menu = document.createElement('div');
- this._menu.className = 'context-menu';
- this._menu.id = 'context-menu';
- document.body.appendChild(this._menu);
-
- document.addEventListener('click', () => this.hide());
- document.addEventListener('contextmenu', (e) => {
- if (!e.target.closest('.tree-item')) {
- this.hide();
- }
- });
-
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') this.hide();
- });
-
- document.addEventListener('scroll', () => this.hide(), true);
- },
-
- show(x, y, vault, path, type, isReadonly) {
- this._targetVault = vault;
- this._targetPath = path;
- this._targetType = type;
-
- this._menu.innerHTML = '';
-
- // Copy path — available for all types
- const pathToCopy = type === 'vault' ? vault : `${vault}/${path}`;
- this._addItem('clipboard-copy', 'Copier le chemin', () => this._copyPath(pathToCopy), false);
-
- // Graph view — available for all types
- const graphPath = type === 'vault' ? '' : path;
- this._addItem('git-graph', 'Vue Graphique', () => GraphViewManager.open(vault, graphPath, type), false);
-
- this._addSeparator();
-
- if (type === 'vault') {
- this._addItem('folder-plus', 'Nouveau dossier', () => this._createDirectory(), isReadonly);
- this._addItem('file-plus', 'Nouveau fichier', () => this._createFile(), isReadonly);
- } else if (type === 'directory') {
- this._addItem('folder-plus', 'Nouveau sous-dossier', () => this._createDirectory(), isReadonly);
- this._addItem('file-plus', 'Nouveau fichier ici', () => this._createFile(), isReadonly);
- this._addSeparator();
- this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
- this._addItem('trash-2', 'Supprimer', () => this._deleteDirectory(), isReadonly);
- } else if (type === 'file') {
- this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
- this._addItem('trash-2', 'Supprimer', () => this._deleteFile(), isReadonly);
- this._addSeparator();
- this._addItem('bookmark-plus', 'Ajouter aux bookmarks', () => this._toggleBookmark(), false);
- }
-
- this._menu.classList.add('active');
-
- const rect = this._menu.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- let finalX = x;
- let finalY = y;
-
- if (x + rect.width > viewportWidth) {
- finalX = viewportWidth - rect.width - 10;
- }
-
- if (y + rect.height > viewportHeight) {
- finalY = viewportHeight - rect.height - 10;
- }
-
- this._menu.style.left = `${finalX}px`;
- this._menu.style.top = `${finalY}px`;
-
- safeCreateIcons();
- },
-
- hide() {
- if (this._menu) {
- this._menu.classList.remove('active');
- }
- },
-
- _addItem(icon, label, callback, disabled) {
- const item = document.createElement('div');
- item.className = 'context-menu-item' + (disabled ? ' disabled' : '');
- item.innerHTML = `
-
- ${label}
- `;
-
- if (!disabled) {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- this.hide();
- callback();
- });
- } else {
- item.title = 'Vault en lecture seule';
- }
-
- this._menu.appendChild(item);
- },
-
- _addSeparator() {
- const sep = document.createElement('div');
- sep.className = 'context-menu-separator';
- this._menu.appendChild(sep);
- },
-
- _createDirectory() {
- FileOperations.showCreateDirectoryModal(this._targetVault, this._targetPath);
- },
-
- _createFile() {
- FileOperations.showCreateFileModal(this._targetVault, this._targetPath);
- },
-
- _renameItem() {
- FileOperations.startInlineRename(this._targetVault, this._targetPath, this._targetType);
- },
-
- _deleteDirectory() {
- FileOperations.confirmDeleteDirectory(this._targetVault, this._targetPath);
- },
-
- _deleteFile() {
- FileOperations.confirmDeleteFile(this._targetVault, this._targetPath);
- },
-
- _copyPath(path) {
- // Try modern clipboard API first, fall back to execCommand for non-secure contexts
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(path).then(() => {
- showToast(`Chemin copié : ${path}`, 'success');
- }).catch(() => {
- this._copyPathFallback(path);
- });
- } else {
- this._copyPathFallback(path);
- }
- },
-
- _copyPathFallback(path) {
- const textarea = document.createElement('textarea');
- textarea.value = path;
- textarea.style.position = 'fixed';
- textarea.style.left = '-9999px';
- textarea.style.top = '-9999px';
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
- try {
- const success = document.execCommand('copy');
- if (success) {
- showToast(`Chemin copié : ${path}`, 'success');
- } else {
- showToast('Erreur lors de la copie', 'error');
- }
- } catch (e) {
- showToast('Erreur lors de la copie', 'error');
- }
- document.body.removeChild(textarea);
- },
-
- async _toggleBookmark() {
- try {
- const data = await api("/api/bookmarks/toggle", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ vault: this._targetVault, path: this._targetPath, title: this._targetPath.split("/").pop() }),
- });
- showToast(data.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");
- if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
- DashboardBookmarkWidget.load();
- }
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- }
- };
-
- // ---------------------------------------------------------------------------
- // File Operations Manager
- // ---------------------------------------------------------------------------
-
- const FileOperations = {
- showCreateDirectoryModal(vault, parentPath) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
-
- const input = modal.querySelector('#dir-name-input');
- const errorDiv = modal.querySelector('#dir-error');
- const createBtn = modal.querySelector('#dir-create-btn');
- const cancelBtn = modal.querySelector('#dir-cancel-btn');
-
- input.focus();
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- input.addEventListener('input', () => {
- const error = validateName(input.value);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- input.classList.add('error');
- } else {
- errorDiv.style.display = 'none';
- input.classList.remove('error');
- }
- });
-
- const create = async () => {
- const name = input.value.trim();
- const error = validateName(name);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- return;
- }
-
- const path = parentPath ? `${parentPath}/${name}` : name;
- createBtn.disabled = true;
- createBtn.textContent = 'Création...';
-
- try {
- await api(`/api/directory/${encodeURIComponent(vault)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path }),
- });
-
- showToast(`Dossier "${name}" créé`, 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
- } catch (err) {
- showToast(err.message || 'Erreur lors de la création', 'error');
- createBtn.disabled = false;
- createBtn.textContent = 'Créer';
- }
- };
-
- createBtn.addEventListener('click', create);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
-
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') create();
- if (e.key === 'Escape') this._closeModal(overlay);
- });
- },
-
- showCreateFileModal(vault, parentPath) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
- Type de fichier
-
- Markdown (.md)
- Texte (.txt)
- Python (.py)
- JavaScript (.js)
- JSON (.json)
- YAML (.yaml)
- Shell (.sh)
- PowerShell (.ps1)
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
-
- const input = modal.querySelector('#file-name-input');
- const extSelect = modal.querySelector('#file-ext-select');
- const errorDiv = modal.querySelector('#file-error');
- const createBtn = modal.querySelector('#file-create-btn');
- const cancelBtn = modal.querySelector('#file-cancel-btn');
-
- input.focus();
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- input.addEventListener('input', () => {
- const error = validateName(input.value);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- input.classList.add('error');
- } else {
- errorDiv.style.display = 'none';
- input.classList.remove('error');
- }
- });
-
- const create = async () => {
- let name = input.value.trim();
- const error = validateName(name);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- return;
- }
-
- const ext = extSelect.value;
- if (!name.endsWith(ext)) {
- name += ext;
- }
-
- const path = parentPath ? `${parentPath}/${name}` : name;
- createBtn.disabled = true;
- createBtn.textContent = 'Création...';
-
- try {
- await api(`/api/file/${encodeURIComponent(vault)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path, content: '' }),
- });
-
- showToast(`Fichier "${name}" créé`, 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
- openFile(vault, path);
- } catch (err) {
- showToast(err.message || 'Erreur lors de la création', 'error');
- createBtn.disabled = false;
- createBtn.textContent = 'Créer';
- }
- };
-
- createBtn.addEventListener('click', create);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
-
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') create();
- if (e.key === 'Escape') this._closeModal(overlay);
- });
- },
-
- async startInlineRename(vault, path, type) {
- const item = document.querySelector(`.tree-item[data-vault="${CSS.escape(vault)}"][data-path="${CSS.escape(path)}"]`);
- if (!item) {
- showToast('Élément introuvable dans l’arborescence', 'error');
- return;
- }
-
- const textNode = Array.from(item.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
- if (!textNode) {
- showToast('Impossible de renommer cet élément', 'error');
- return;
- }
-
- const originalText = textNode.textContent;
- const trimmedOriginal = originalText.trim();
- const currentName = path.split('/').pop() || trimmedOriginal;
- const baseName = type === 'file' ? currentName.replace(/(\.[^./\\]+)$/i, '') : currentName;
- const extension = type === 'file' ? (currentName.match(/(\.[^./\\]+)$/i)?.[1] || '') : '';
-
- const input = document.createElement('input');
- input.type = 'text';
- input.className = 'sidebar-item-input';
- input.value = baseName;
-
- textNode.textContent = ' ';
- const badge = item.querySelector('.badge-small');
- if (badge) {
- item.insertBefore(input, badge);
- } else {
- item.appendChild(input);
- }
-
- const restore = () => {
- input.remove();
- textNode.textContent = originalText;
- };
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- const submit = async () => {
- const name = input.value.trim();
- const error = validateName(name);
- if (error) {
- showToast(error, 'error');
- input.focus();
- input.select();
- return;
- }
-
- const newName = `${name}${extension}`;
- if (newName === currentName) {
- restore();
- return;
- }
-
- input.disabled = true;
- try {
- const endpoint = type === 'directory' ? `/api/directory/${encodeURIComponent(vault)}` : `/api/file/${encodeURIComponent(vault)}`;
- const result = await api(endpoint, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path, new_name: newName }),
- });
-
- const nextPath = result.new_path;
- await refreshSidebarTreePreservingState();
-
- if (type === 'file' && currentVault === vault && currentPath === path) {
- await openFile(vault, nextPath);
- } else if (type === 'directory' && currentVault === vault && currentPath && (currentPath === path || currentPath.startsWith(`${path}/`))) {
- const suffix = currentPath === path ? '' : currentPath.slice(path.length);
- currentPath = `${nextPath}${suffix}`;
- await focusPathInSidebar(vault, currentPath, { alignToTop: false });
- }
-
- showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');
- } catch (err) {
- input.disabled = false;
- showToast(err.message || 'Erreur lors du renommage', 'error');
- input.focus();
- input.select();
- return;
- }
- };
-
- input.addEventListener('click', (e) => e.stopPropagation());
- input.addEventListener('keydown', async (e) => {
- e.stopPropagation();
- if (e.key === 'Enter') {
- e.preventDefault();
- await submit();
- }
- if (e.key === 'Escape') {
- e.preventDefault();
- restore();
- }
- });
- input.addEventListener('blur', async () => {
- if (!input.disabled) {
- await submit();
- }
- });
-
- input.focus();
- input.setSelectionRange(0, input.value.length);
- },
-
- confirmDeleteDirectory(vault, path) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
-
- Attention ! Cette action est irréversible.
- Tous les fichiers et sous-dossiers seront supprimés définitivement.
-
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
- safeCreateIcons();
-
- const confirmBtn = modal.querySelector('#del-confirm-btn');
- const cancelBtn = modal.querySelector('#del-cancel-btn');
-
- const deleteDir = async () => {
- confirmBtn.disabled = true;
- confirmBtn.textContent = 'Suppression...';
-
- try {
- const result = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
- method: 'DELETE',
- });
-
- showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
-
- if (currentVault === vault && currentPath && currentPath.startsWith(path)) {
- showWelcome();
- }
- } catch (err) {
- showToast(err.message || 'Erreur lors de la suppression', 'error');
- confirmBtn.disabled = false;
- confirmBtn.textContent = 'Supprimer définitivement';
- }
- };
-
- confirmBtn.addEventListener('click', deleteDir);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
- },
-
- confirmDeleteFile(vault, path) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
-
- Attention ! Cette action est irréversible.
-
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
- safeCreateIcons();
-
- const confirmBtn = modal.querySelector('#del-confirm-btn');
- const cancelBtn = modal.querySelector('#del-cancel-btn');
-
- const deleteFile = async () => {
- confirmBtn.disabled = true;
- confirmBtn.textContent = 'Suppression...';
-
- try {
- await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
- method: 'DELETE',
- });
-
- showToast('Fichier supprimé', 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
-
- if (currentVault === vault && currentPath === path) {
- showWelcome();
- }
- } catch (err) {
- showToast(err.message || 'Erreur lors de la suppression', 'error');
- confirmBtn.disabled = false;
- confirmBtn.textContent = 'Supprimer définitivement';
- }
- };
-
- confirmBtn.addEventListener('click', deleteFile);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
- },
-
- _createModalOverlay() {
- const overlay = document.createElement('div');
- overlay.className = 'obsigate-modal-overlay';
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) {
- this._closeModal(overlay);
- }
- });
- return overlay;
- },
-
- _closeModal(overlay) {
- overlay.classList.remove('active');
- setTimeout(() => overlay.remove(), 200);
- }
- };
-
- // ---------------------------------------------------------------------------
- // Find in Page Manager
- // ---------------------------------------------------------------------------
- const FindInPageManager = {
- isOpen: false,
- searchTerm: "",
- matches: [],
- currentIndex: -1,
- options: {
- caseSensitive: false,
- wholeWord: false,
- useRegex: false,
- },
- debounceTimer: null,
- previousFocus: null,
-
- init() {
- const bar = document.getElementById("find-in-page-bar");
- const input = document.getElementById("find-input");
- const prevBtn = document.getElementById("find-prev");
- const nextBtn = document.getElementById("find-next");
- const closeBtn = document.getElementById("find-close");
- const caseSensitiveBtn = document.getElementById("find-case-sensitive");
- const wholeWordBtn = document.getElementById("find-whole-word");
- const regexBtn = document.getElementById("find-regex");
-
- if (!bar || !input) return;
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- // Ctrl+F or Cmd+F to open
- if ((e.ctrlKey || e.metaKey) && e.key === "f") {
- e.preventDefault();
- this.open();
- }
- // Escape to close
- if (e.key === "Escape" && this.isOpen) {
- e.preventDefault();
- this.close();
- }
- // Enter to go to next
- if (e.key === "Enter" && this.isOpen && document.activeElement === input) {
- e.preventDefault();
- if (e.shiftKey) {
- this.goToPrevious();
- } else {
- this.goToNext();
- }
- }
- // F3 for next/previous
- if (e.key === "F3" && this.isOpen) {
- e.preventDefault();
- if (e.shiftKey) {
- this.goToPrevious();
- } else {
- this.goToNext();
- }
- }
- });
-
- // Input event with debounce
- input.addEventListener("input", (e) => {
- clearTimeout(this.debounceTimer);
- this.debounceTimer = setTimeout(() => {
- this.search(e.target.value);
- }, 250);
- });
-
- // Navigation buttons
- prevBtn.addEventListener("click", () => this.goToPrevious());
- nextBtn.addEventListener("click", () => this.goToNext());
-
- // Close button
- closeBtn.addEventListener("click", () => this.close());
-
- // Option toggles
- caseSensitiveBtn.addEventListener("click", () => {
- this.options.caseSensitive = !this.options.caseSensitive;
- caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- wholeWordBtn.addEventListener("click", () => {
- this.options.wholeWord = !this.options.wholeWord;
- wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- regexBtn.addEventListener("click", () => {
- this.options.useRegex = !this.options.useRegex;
- regexBtn.setAttribute("aria-pressed", this.options.useRegex);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- // Load saved state
- this.loadState();
- },
-
- open() {
- const bar = document.getElementById("find-in-page-bar");
- const input = document.getElementById("find-input");
- if (!bar || !input) return;
-
- this.previousFocus = document.activeElement;
- this.isOpen = true;
- bar.hidden = false;
- input.focus();
- input.select();
- safeCreateIcons();
- },
-
- close() {
- const bar = document.getElementById("find-in-page-bar");
- if (!bar) return;
-
- this.isOpen = false;
- bar.hidden = true;
- this.clearHighlights();
- this.matches = [];
- this.currentIndex = -1;
- this.searchTerm = "";
-
- // Restore previous focus
- if (this.previousFocus && this.previousFocus.focus) {
- this.previousFocus.focus();
- }
- },
-
- search(term) {
- this.searchTerm = term;
- this.clearHighlights();
- this.hideError();
-
- if (!term || term.trim().length === 0) {
- this.updateCounter();
- this.updateNavButtons();
- return;
- }
-
- const contentArea = document.querySelector(".md-content");
- if (!contentArea) {
- this.updateCounter();
- this.updateNavButtons();
- return;
- }
-
- try {
- const regex = this.createRegex(term);
- this.matches = [];
- this.findMatches(contentArea, regex);
- this.currentIndex = this.matches.length > 0 ? 0 : -1;
- this.highlightMatches();
- this.updateCounter();
- this.updateNavButtons();
-
- if (this.matches.length > 0) {
- this.scrollToMatch(0);
- }
- } catch (err) {
- this.showError(err.message);
- this.matches = [];
- this.currentIndex = -1;
- this.updateCounter();
- this.updateNavButtons();
- }
- },
-
- createRegex(term) {
- let pattern = term;
-
- if (!this.options.useRegex) {
- // Escape special regex characters
- pattern = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- }
-
- if (this.options.wholeWord) {
- pattern = "\\b" + pattern + "\\b";
- }
-
- const flags = this.options.caseSensitive ? "g" : "gi";
- return new RegExp(pattern, flags);
- },
-
- findMatches(container, regex) {
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
- acceptNode: (node) => {
- // Skip code blocks, scripts, styles
- const parent = node.parentElement;
- if (!parent) return NodeFilter.FILTER_REJECT;
- const tagName = parent.tagName.toLowerCase();
- if (["code", "pre", "script", "style"].includes(tagName)) {
- return NodeFilter.FILTER_REJECT;
- }
- // Skip empty text nodes
- if (!node.textContent || node.textContent.trim().length === 0) {
- return NodeFilter.FILTER_REJECT;
- }
- return NodeFilter.FILTER_ACCEPT;
- },
- });
-
- let node;
- while ((node = walker.nextNode())) {
- const text = node.textContent;
- let match;
- regex.lastIndex = 0; // Reset regex
-
- while ((match = regex.exec(text)) !== null) {
- this.matches.push({
- node: node,
- index: match.index,
- length: match[0].length,
- text: match[0],
- });
-
- // Prevent infinite loop with zero-width matches
- if (match.index === regex.lastIndex) {
- regex.lastIndex++;
- }
- }
- }
- },
-
- highlightMatches() {
- const matchesByNode = new Map();
-
- this.matches.forEach((match, idx) => {
- if (!matchesByNode.has(match.node)) {
- matchesByNode.set(match.node, []);
- }
- matchesByNode.get(match.node).push({ match, idx });
- });
-
- matchesByNode.forEach((entries, node) => {
- if (!node || !node.parentNode) return;
-
- const text = node.textContent || "";
- let cursor = 0;
- const fragment = document.createDocumentFragment();
-
- entries.sort((a, b) => a.match.index - b.match.index);
-
- entries.forEach(({ match, idx }) => {
- if (match.index > cursor) {
- fragment.appendChild(document.createTextNode(text.substring(cursor, match.index)));
- }
-
- const matchText = text.substring(match.index, match.index + match.length);
- const mark = document.createElement("mark");
- mark.className = idx === this.currentIndex ? "find-highlight find-highlight-active" : "find-highlight";
- mark.textContent = matchText;
- mark.setAttribute("data-find-index", idx);
- fragment.appendChild(mark);
-
- match.element = mark;
- cursor = match.index + match.length;
- });
-
- if (cursor < text.length) {
- fragment.appendChild(document.createTextNode(text.substring(cursor)));
- }
-
- node.parentNode.replaceChild(fragment, node);
- });
- },
-
- clearHighlights() {
- const contentArea = document.querySelector(".md-content");
- if (!contentArea) return;
-
- const marks = contentArea.querySelectorAll("mark.find-highlight");
- marks.forEach((mark) => {
- if (!mark.parentNode) return;
- const text = mark.textContent;
- const textNode = document.createTextNode(text);
- mark.parentNode.replaceChild(textNode, mark);
- });
-
- // Normalize text nodes to merge adjacent text nodes
- contentArea.normalize();
- },
-
- goToNext() {
- if (this.matches.length === 0) return;
-
- // Remove active class from current
- if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.remove("find-highlight-active");
- }
-
- // Move to next (with wrapping)
- this.currentIndex = (this.currentIndex + 1) % this.matches.length;
-
- // Add active class to new current
- if (this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.add("find-highlight-active");
- }
-
- this.scrollToMatch(this.currentIndex);
- this.updateCounter();
- },
-
- goToPrevious() {
- if (this.matches.length === 0) return;
-
- // Remove active class from current
- if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.remove("find-highlight-active");
- }
-
- // Move to previous (with wrapping)
- this.currentIndex = this.currentIndex <= 0 ? this.matches.length - 1 : this.currentIndex - 1;
-
- // Add active class to new current
- if (this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.add("find-highlight-active");
- }
-
- this.scrollToMatch(this.currentIndex);
- this.updateCounter();
- },
-
- scrollToMatch(index) {
- if (index < 0 || index >= this.matches.length) return;
-
- const match = this.matches[index];
- if (!match.element) return;
-
- const contentArea = document.getElementById("content-area");
- if (!contentArea) {
- match.element.scrollIntoView({ behavior: "smooth", block: "center" });
- return;
- }
-
- // Calculate position with offset for header
- const elementTop = match.element.offsetTop;
- const offset = 100; // Offset for header
-
- contentArea.scrollTo({
- top: elementTop - offset,
- behavior: "smooth",
- });
- },
-
- updateCounter() {
- const counter = document.getElementById("find-counter");
- if (!counter) return;
-
- const count = this.matches.length;
- if (count === 0) {
- counter.textContent = "0 occurrence";
- } else if (count === 1) {
- counter.textContent = "1 occurrence";
- } else {
- counter.textContent = `${count} occurrences`;
- }
- },
-
- updateNavButtons() {
- const prevBtn = document.getElementById("find-prev");
- const nextBtn = document.getElementById("find-next");
- if (!prevBtn || !nextBtn) return;
-
- const hasMatches = this.matches.length > 0;
- prevBtn.disabled = !hasMatches;
- nextBtn.disabled = !hasMatches;
- },
-
- showError(message) {
- const errorEl = document.getElementById("find-error");
- if (!errorEl) return;
-
- errorEl.textContent = message;
- errorEl.hidden = false;
- },
-
- hideError() {
- const errorEl = document.getElementById("find-error");
- if (!errorEl) return;
-
- errorEl.hidden = true;
- },
-
- saveState() {
- try {
- const state = {
- options: this.options,
- };
- localStorage.setItem("obsigate-find-in-page-state", JSON.stringify(state));
- } catch (e) {
- // Ignore localStorage errors
- }
- },
-
- loadState() {
- try {
- const saved = localStorage.getItem("obsigate-find-in-page-state");
- if (saved) {
- const state = JSON.parse(saved);
- if (state.options) {
- this.options = { ...this.options, ...state.options };
-
- // Update button states
- const caseSensitiveBtn = document.getElementById("find-case-sensitive");
- const wholeWordBtn = document.getElementById("find-whole-word");
- const regexBtn = document.getElementById("find-regex");
-
- if (caseSensitiveBtn) caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive);
- if (wholeWordBtn) wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord);
- if (regexBtn) regexBtn.setAttribute("aria-pressed", this.options.useRegex);
- }
- }
- } catch (e) {
- // Ignore localStorage errors
- }
- },
- };
-
- // ---------------------------------------------------------------------------
- // Init
- // ---------------------------------------------------------------------------
- async function init() {
- initTheme();
- initHeaderMenu();
- initCustomDropdowns();
- document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
- document.getElementById("header-logo").addEventListener("click", goHome);
- const refreshBtn = document.getElementById("header-refresh-btn");
- if (refreshBtn) refreshBtn.addEventListener("click", goHome);
- initSearch();
- initSidebarToggle();
- initMobile();
- initVaultContext();
- initSidebarTabs();
- initHelpModal();
- initConfigModal();
- initEditorPocBtn();
- initSidebarFilter();
- initSidebarResize();
- initEditor();
- initLoginForm();
- initRecentTab();
- RightSidebarManager.init();
- FindInPageManager.init();
- ContextMenuManager.init();
-
- // Mermaid.js init
- import('./mermaid-viewer.js').then(function(m) {
- m.initMermaid();
- m.setupFocusMode();
- });
-
- // Check auth status first
- const authOk = await AuthManager.initAuth();
-
- if (authOk) {
- // Start SSE sync AFTER auth is established (cookie available)
- initSyncStatus();
-
- try {
- await Promise.all([loadVaultSettings(), loadVaults(), loadTags()]);
-
- // Initialize dashboard widgets now that vaults are loaded
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.init();
- }
-
- // Check for popup mode query parameter
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get("popup") === "true") {
- document.body.classList.add("popup-mode");
- }
-
- // Handle direct deep-link to file via #file=...
- if (window.location.hash && window.location.hash.startsWith("#file=")) {
- const hashVal = window.location.hash.substring(6);
- const sepIndex = hashVal.indexOf(":");
- if (sepIndex > -1) {
- const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
- const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
- openFile(vault, path);
- }
- } else if (urlParams.get("popup") !== "true") {
- // Default to dashboard if no deep link and not in popup mode
- showWelcome();
- }
- } catch (err) {
- console.error("Failed to initialize ObsiGate:", err);
- showToast("Erreur lors de l'initialisation", "error");
- }
- }
-
- safeCreateIcons();
- }
-
- // ---------------------------------------------------------------------------
- // PWA Service Worker Registration
- // ---------------------------------------------------------------------------
- function registerServiceWorker() {
- if ("serviceWorker" in navigator) {
- window.addEventListener("load", () => {
- navigator.serviceWorker
- .register("/sw.js")
- .then((registration) => {
- console.log("[PWA] Service Worker registered successfully:", registration.scope);
-
- // Check for updates periodically
- setInterval(() => {
- registration.update();
- }, 60000); // Check every minute
-
- // Handle service worker updates
- registration.addEventListener("updatefound", () => {
- const newWorker = registration.installing;
- newWorker.addEventListener("statechange", () => {
- if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
- // New service worker available
- showUpdateNotification();
- }
- });
- });
- })
- .catch((error) => {
- console.log("[PWA] Service Worker registration failed:", error);
- });
- });
- }
- }
-
- function showUpdateNotification() {
- const message = document.createElement("div");
- message.className = "pwa-update-notification";
- message.innerHTML = `
-
- Une nouvelle version d'ObsiGate est disponible !
- Mettre à jour
- ×
-
- `;
- document.body.appendChild(message);
-
- // Auto-dismiss after 30 seconds
- setTimeout(() => {
- if (message.parentElement) {
- message.remove();
- }
- }, 30000);
- }
-
- // Handle install prompt
- let deferredPrompt;
- window.addEventListener("beforeinstallprompt", (e) => {
- e.preventDefault();
- deferredPrompt = e;
-
- // Show install button if desired
- const installBtn = document.getElementById("pwa-install-btn");
- if (installBtn) {
- installBtn.style.display = "block";
- installBtn.addEventListener("click", async () => {
- if (deferredPrompt) {
- deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
- console.log(`[PWA] User response to install prompt: ${outcome}`);
- deferredPrompt = null;
- installBtn.style.display = "none";
- }
- });
- }
- });
-
- // Log when app is installed
- window.addEventListener("appinstalled", () => {
- console.log("[PWA] ObsiGate has been installed");
- showToast("ObsiGate installé avec succès !", "success");
- });
-
- document.addEventListener("DOMContentLoaded", () => {
- init();
- registerServiceWorker();
- initDashboardTabs();
- });
-
- // ── Dashboard tab switching (runs on page load and after rebuild) ──
- function initDashboardTabs() {
- document.querySelectorAll(".dashboard-tab").forEach(tab => {
- // Remove existing listeners by cloning
- const newTab = tab.cloneNode(true);
- tab.parentNode.replaceChild(newTab, tab);
- newTab.addEventListener("click", function() {
- document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
- document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
- this.classList.add("active");
- const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
- if (panel) panel.classList.add("active");
- });
- });
- }
-
- // ---------------------------------------------------------------------------
- // Tab Manager — Multi-file tab support
- // ---------------------------------------------------------------------------
-
- const TabManager = {
- _tabs: [],
- _activeTabId: null,
- _previewTabId: null, // single-click preview tab (temporary, replaced on next preview)
- _tabCache: {}, // { tabId: { vault, path, title, data, rawSource, sourceView, scrollTop, icon } }
- _tabBar: null,
- _tabList: null,
- _dirtyTabs: new Set(),
-
- init() {
- this._tabBar = document.getElementById("tab-bar");
- this._tabList = document.getElementById("tab-list");
- },
-
- /** Open a file as a preview tab (single-click).
- * Replaces any existing preview tab. If the file is already
- * open as a persistent tab, just activates it. */
- async openPreview(vault, path) {
- const tabId = `${vault}::${path}`;
-
- // If already open as persistent tab, just activate it
- const existing = this._tabs.find(t => t.id === tabId && !t.preview);
- if (existing) {
- this.activate(tabId);
- return;
- }
-
- // Close existing preview tab
- if (this._previewTabId && this._previewTabId !== tabId) {
- this.close(this._previewTabId);
- }
-
- // If already open as preview, just focus it
- const previewExisting = this._tabs.find(t => t.id === tabId && t.preview);
- if (previewExisting) {
- this.activate(tabId);
- return;
- }
-
- // Create preview tab
- const name = path.split("/").pop().replace(/\.md$/i, "");
- const icon = getFileIcon(name + ".md");
-
- this._tabs.push({ id: tabId, vault, path, name, icon, preview: true });
- this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
- this._previewTabId = tabId;
-
- this._renderTabs();
- this.activate(tabId);
- },
-
- /** Convert a preview tab to a persistent tab (double-click).
- * If already persistent, opens a new duplicate (same file, different tab). */
- async openPersistent(vault, path) {
- const tabId = `${vault}::${path}`;
-
- // If it's already a preview tab, convert it to persistent
- const previewTab = this._tabs.find(t => t.id === tabId && t.preview);
- if (previewTab) {
- previewTab.preview = false;
- if (this._previewTabId === tabId) {
- this._previewTabId = null;
- }
- this._renderTabs();
- this.activate(tabId);
- return;
- }
-
- // If already persistent, just focus it
- const existing = this._tabs.find(t => t.id === tabId && !t.preview);
- if (existing) {
- this.activate(tabId);
- return;
- }
-
- // Create a new persistent tab
- this.open(vault, path);
- },
-
- /** Open a file in a tab (or focus existing) */
- async open(vault, path, options = {}) {
- const tabId = `${vault}::${path}`;
-
- // If already open, just focus it
- const existing = this._tabs.find(t => t.id === tabId);
- if (existing) {
- // Convert preview to persistent if needed
- if (existing.preview) {
- existing.preview = false;
- if (this._previewTabId === tabId) this._previewTabId = null;
- this._renderTabs();
- }
- this.activate(tabId);
- return;
- }
-
- // Create new tab
- const name = path.split("/").pop().replace(/\.md$/i, "");
- const icon = getFileIcon(name + ".md");
-
- this._tabs.push({ id: tabId, vault, path, name, icon });
- this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
-
- this._renderTabs();
- this.activate(tabId);
- },
-
- /** Activate a specific tab */
- async activate(tabId) {
- if (this._activeTabId === tabId && this._tabs.length > 0) return;
-
- // Save current tab state
- if (this._activeTabId && this._tabCache[this._activeTabId]) {
- this._saveCurrentTabState();
- }
-
- this._activeTabId = tabId;
- this._renderTabs();
-
- // Load tab content
- const cache = this._tabCache[tabId];
- if (!cache) return;
-
- // Update global state
- currentVault = cache.vault;
- currentPath = cache.path;
- syncActiveFileTreeItem(cache.vault, cache.path);
-
- const area = document.getElementById("content-area");
-
- if (cache.data) {
- // Use cached data
- this._restoreTabContent(cache, area);
- } else {
- // Fetch file content
- area.innerHTML = 'Chargement...
';
- try {
- const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`);
- cache.data = data;
- cache.title = data.title;
- renderFile(cache.data);
-
- // Restore source view if needed
- if (cache.sourceView) {
- await this._toggleSourceView(cache, area);
- }
- if (cache.scrollTop) {
- area.scrollTop = cache.scrollTop;
- }
- } catch (err) {
- area.innerHTML = `Erreur: ${escapeHtml(err.message)}
`;
- }
- }
-
- // Update URL hash
- if (history.pushState) {
- history.pushState(null, "", `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`);
- }
-
- // Hide dashboard
- const dashboard = document.getElementById("dashboard-home");
- if (dashboard) dashboard.style.display = "none";
- },
-
- /** Close a tab */
- close(tabId) {
- const idx = this._tabs.findIndex(t => t.id === tabId);
- if (idx === -1) return;
-
- this._tabs.splice(idx, 1);
- delete this._tabCache[tabId];
- this._dirtyTabs.delete(tabId);
-
- if (this._tabs.length === 0) {
- this._activeTabId = null;
- this._showDashboard();
- this._tabBar.hidden = true;
- } else if (this._activeTabId === tabId) {
- // Activate adjacent tab
- const newIdx = Math.min(idx, this._tabs.length - 1);
- this.activate(this._tabs[newIdx].id);
- }
-
- this._renderTabs();
- },
-
- /** Close all tabs */
- closeAll() {
- this._tabs = [];
- this._tabCache = {};
- this._dirtyTabs.clear();
- this._activeTabId = null;
- this._showDashboard();
- this._tabBar.hidden = true;
- },
-
- /** Close tabs to the right */
- closeRight(tabId) {
- const idx = this._tabs.findIndex(t => t.id === tabId);
- if (idx === -1) return;
- const toClose = this._tabs.slice(idx + 1);
- for (const tab of toClose) {
- delete this._tabCache[tab.id];
- this._dirtyTabs.delete(tab.id);
- }
- this._tabs = this._tabs.slice(0, idx + 1);
- if (!this._tabs.find(t => t.id === this._activeTabId)) {
- this.activate(tabId);
- }
- this._renderTabs();
- },
-
- /** Close other tabs */
- closeOthers(tabId) {
- const tab = this._tabs.find(t => t.id === tabId);
- if (!tab) return;
- for (const t of this._tabs) {
- if (t.id !== tabId) {
- delete this._tabCache[t.id];
- this._dirtyTabs.delete(t.id);
- }
- }
- this._tabs = [tab];
- this.activate(tabId);
- this._renderTabs();
- },
-
- /** Reorder tabs by drag and drop */
- moveTab(fromIdx, toIdx) {
- if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return;
- const tab = this._tabs.splice(fromIdx, 1)[0];
- this._tabs.splice(toIdx, 0, tab);
- this._renderTabs();
- },
-
- /** Save current tab state before switching */
- _saveCurrentTabState() {
- const cache = this._tabCache[this._activeTabId];
- if (!cache) return;
-
- const area = document.getElementById("content-area");
- const rendered = document.getElementById("file-rendered-content");
-
- cache.scrollTop = area.scrollTop;
- cache.sourceView = rendered ? rendered.style.display === "none" : false;
- },
-
- /** Restore tab content from cache */
- _restoreTabContent(cache, area) {
- renderFile(cache.data);
- if (cache.sourceView) {
- this._restoreSourceView(cache, area);
- }
- if (cache.scrollTop) {
- area.scrollTop = cache.scrollTop;
- }
- },
-
- async _toggleSourceView(cache, area) {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (!rendered || !raw) return;
-
- if (!cache.rawSource) {
- const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`);
- cache.rawSource = rawData.raw;
- }
- raw.textContent = cache.rawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- },
-
- _restoreSourceView(cache, area) {
- requestAnimationFrame(() => {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (rendered && raw && cache.rawSource) {
- raw.textContent = cache.rawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- }
- });
- },
-
- _showDashboard() {
- const area = document.getElementById("content-area");
- // Save dashboard DOM before clearing (it may have been removed from DOM by renderFile)
- let dashboard = document.getElementById("dashboard-home");
- if (!dashboard) {
- // Dashboard was destroyed — rebuild via showWelcome
- area.innerHTML = "";
- showWelcome();
- return;
- }
- area.innerHTML = "";
- dashboard.style.display = "";
- area.appendChild(dashboard);
- // Refresh widgets after restoring
- if (typeof DashboardStatsWidget !== "undefined") DashboardStatsWidget.load();
- if (typeof DashboardConflictsWidget !== "undefined") DashboardConflictsWidget.load();
- if (typeof DashboardRecentWidget !== "undefined") DashboardRecentWidget.load(selectedContextVault);
- if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load(selectedContextVault);
- if (history.pushState) {
- history.pushState(null, "", "#");
- }
- },
-
- /** Render the tab bar */
- _renderTabs() {
- if (!this._tabList) return;
-
- this._tabList.innerHTML = "";
-
- if (this._tabs.length === 0) {
- this._tabBar.hidden = true;
- return;
- }
-
- this._tabBar.hidden = false;
-
- this._tabs.forEach((tab, idx) => {
- const el = document.createElement("div");
- el.className = "tab-item" + (tab.id === this._activeTabId ? " active" : "") + (tab.preview ? " preview" : "");
- el.draggable = true;
- el.dataset.tabId = tab.id;
- el.dataset.index = idx;
-
- // Icon
- const iconEl = document.createElement("i");
- iconEl.setAttribute("data-lucide", tab.icon);
- iconEl.className = "tab-icon";
- iconEl.style.width = "14px";
- iconEl.style.height = "14px";
- el.appendChild(iconEl);
-
- // Name
- const nameEl = document.createElement("span");
- nameEl.className = "tab-name";
- nameEl.textContent = tab.name;
- nameEl.title = `${tab.vault}/${tab.path}`;
- el.appendChild(nameEl);
-
- // Close button
- const closeEl = document.createElement("span");
- closeEl.className = "tab-close";
- closeEl.innerHTML = ' ';
- closeEl.addEventListener("click", (e) => {
- e.stopPropagation();
- this.close(tab.id);
- });
- el.appendChild(closeEl);
-
- // Click to activate
- el.addEventListener("click", () => this.activate(tab.id));
-
- // Double-click to close
- el.addEventListener("dblclick", (e) => {
- e.preventDefault();
- this.close(tab.id);
- });
-
- // Middle-click to close
- el.addEventListener("mousedown", (e) => {
- if (e.button === 1) {
- e.preventDefault();
- this.close(tab.id);
- }
- });
-
- // Context menu on tab
- el.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- this._showTabContextMenu(e.clientX, e.clientY, tab.id);
- });
-
- // Drag and drop
- el.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", String(idx));
- el.classList.add("dragging");
- });
- el.addEventListener("dragend", () => {
- el.classList.remove("dragging");
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- });
- el.addEventListener("dragover", (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- const rect = el.getBoundingClientRect();
- const mid = rect.left + rect.width / 2;
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- const indicator = document.createElement("div");
- indicator.className = "tab-drop-indicator";
- if (e.clientX < mid) {
- el.before(indicator);
- } else {
- el.after(indicator);
- }
- });
- el.addEventListener("drop", (e) => {
- e.preventDefault();
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- const fromIdx = parseInt(e.dataTransfer.getData("text/plain"));
- const rect = el.getBoundingClientRect();
- const mid = rect.left + rect.width / 2;
- const toIdx = e.clientX < mid ? idx : idx + 1;
- if (fromIdx !== toIdx && fromIdx !== toIdx - 1) {
- this.moveTab(fromIdx, toIdx);
- }
- });
-
- this._tabList.appendChild(el);
- });
-
- safeCreateIcons();
- },
-
- _showTabContextMenu(x, y, tabId) {
- const existing = document.getElementById("tab-context-menu");
- if (existing) existing.remove();
-
- const menu = document.createElement("div");
- menu.id = "tab-context-menu";
- menu.className = "context-menu active";
- menu.style.left = x + "px";
- menu.style.top = y + "px";
- menu.innerHTML = `
-
-
-
-
-
- `;
- document.body.appendChild(menu);
- safeCreateIcons();
-
- menu.addEventListener("click", (e) => {
- const action = e.target.closest(".context-menu-item")?.dataset.action;
- if (action === "close") this.close(tabId);
- else if (action === "closeOthers") this.closeOthers(tabId);
- else if (action === "closeRight") this.closeRight(tabId);
- else if (action === "closeAll") this.closeAll();
- menu.remove();
- });
-
- const closeMenu = () => menu.remove();
- document.addEventListener("click", closeMenu, { once: true });
- document.addEventListener("keydown", (e) => { if (e.key === "Escape") { menu.remove(); } }, { once: true });
- },
- };
-
- // ---- Modify openFile to use TabManager ----
- const _originalOpenFile = openFile;
- openFile = function(vault, path) {
- TabManager.open(vault, path);
- };
-
- // ---- Keyboard shortcuts for tabs ----
- document.addEventListener("keydown", (e) => {
- if (e.ctrlKey || e.metaKey) {
- if (e.key === "w" || e.key === "W") {
- e.preventDefault();
- if (TabManager._activeTabId) {
- TabManager.close(TabManager._activeTabId);
- }
- } else if (e.key === "Tab" && !e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const nextIdx = (currentIdx + 1) % tabs.length;
- TabManager.activate(tabs[nextIdx].id);
- }
- } else if (e.key === "Tab" && e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length;
- TabManager.activate(tabs[prevIdx].id);
- }
- }
- }
- });
-
- // ---- Modify init to include TabManager ----
- const _origInit2 = init;
- init = function() {
- _origInit2();
- TabManager.init();
- };
-
- // ---------------------------------------------------------------------------
- // Graph View Manager — Interactive file/folder relationship visualization
- // ---------------------------------------------------------------------------
-
- const GraphViewManager = {
- _canvas: null,
- _ctx: null,
- _nodes: [],
- _edges: [],
- _offsetX: 0,
- _offsetY: 0,
- _zoom: 1,
- _dragging: false,
- _dragNode: null,
- _panning: false,
- _panStartX: 0,
- _panStartY: 0,
- _animFrame: null,
- _vault: null,
- _path: null,
- _nodePositions: {},
- _width: 0,
- _height: 0,
-
- async open(vault, path, type) {
- this._vault = vault;
- this._path = path;
-
- const modal = document.getElementById("graph-modal");
- const title = document.getElementById("graph-title");
- const info = document.getElementById("graph-info");
- const canvas = document.getElementById("graph-canvas");
-
- if (!modal || !canvas) return;
-
- title.textContent = `Vue Graphique — ${vault}${path ? "/" + path : ""}`;
- info.textContent = "Chargement...";
- modal.classList.add("active");
-
- this._canvas = canvas;
- this._ctx = canvas.getContext("2d");
- this._resetView();
-
- // Fetch graph data
- try {
- const data = await api(`/api/graph/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}&depth=1`);
- this._nodes = data.nodes || [];
- this._edges = data.edges || [];
- info.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens`;
- this._initLayout();
- this._startRender();
- } catch (err) {
- info.textContent = "Erreur de chargement";
- console.error("Graph error:", err);
- }
-
- safeCreateIcons();
- },
-
- close() {
- const modal = document.getElementById("graph-modal");
- if (modal) modal.classList.remove("active");
- if (this._animFrame) {
- cancelAnimationFrame(this._animFrame);
- this._animFrame = null;
- }
- },
-
- _resetView() {
- this._offsetX = 0;
- this._offsetY = 0;
- this._zoom = 1;
- this._nodePositions = {};
- },
-
- _initLayout() {
- const w = this._canvas.parentElement.clientWidth;
- const h = this._canvas.parentElement.clientHeight;
- this._canvas.width = w * devicePixelRatio;
- this._canvas.height = h * devicePixelRatio;
- this._canvas.style.width = w + "px";
- this._canvas.style.height = h + "px";
- this._width = w;
- this._height = h;
- this._ctx.scale(devicePixelRatio, devicePixelRatio);
-
- // Position nodes in a circle initially
- const cx = w / 2;
- const cy = h / 2;
- const radius = Math.min(w, h) * 0.35;
-
- this._nodes.forEach((node, i) => {
- const angle = (2 * Math.PI * i) / this._nodes.length;
- this._nodePositions[node.id] = {
- x: cx + radius * Math.cos(angle),
- y: cy + radius * Math.sin(angle),
- vx: 0,
- vy: 0,
- };
- });
- },
-
- _startRender() {
- const self = this;
- let lastTime = 0;
-
- const loop = (time) => {
- const dt = Math.min((time - lastTime) / 1000, 0.1);
- lastTime = time;
- self._simulate(dt);
- self._draw();
- self._animFrame = requestAnimationFrame(loop);
- };
-
- this._animFrame = requestAnimationFrame(loop);
- },
-
- _simulate(dt) {
- if (this._dragging || this._dragNode) return;
-
- const positions = this._nodePositions;
- const cx = this._width / 2;
- const cy = this._height / 2;
-
- // Spring forces (edges)
- for (const edge of this._edges) {
- const a = positions[edge.source];
- const b = positions[edge.target];
- if (!a || !b) continue;
-
- const dx = b.x - a.x;
- const dy = b.y - a.y;
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
- const targetLen = 80;
- const force = (dist - targetLen) * 0.01;
- const fx = (dx / dist) * force;
- const fy = (dy / dist) * force;
-
- a.vx += fx;
- a.vy += fy;
- b.vx -= fx;
- b.vy -= fy;
- }
-
- // Repulsion between all nodes
- for (const n1 of this._nodes) {
- for (const n2 of this._nodes) {
- if (n1.id === n2.id) continue;
- const a = positions[n1.id];
- const b = positions[n2.id];
- if (!a || !b) continue;
-
- const dx = b.x - a.x;
- const dy = b.y - a.y;
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
- const force = 2000 / (dist * dist);
- const fx = (dx / dist) * force;
- const fy = (dy / dist) * force;
-
- a.vx -= fx;
- a.vy -= fy;
- }
- }
-
- // Center gravity
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- p.vx += (cx - p.x) * 0.001;
- p.vy += (cy - p.y) * 0.001;
- }
-
- // Apply velocities with damping
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- p.vx *= 0.9;
- p.vy *= 0.9;
- p.x += p.vx * dt * 60;
- p.y += p.vy * dt * 60;
- }
- },
-
- _draw() {
- const ctx = this._ctx;
- const w = this._width;
- const h = this._height;
-
- ctx.save();
- ctx.clearRect(0, 0, w, h);
-
- // Apply transform (pan + zoom)
- ctx.translate(this._offsetX, this._offsetY);
- ctx.scale(this._zoom, this._zoom);
-
- // Draw edges
- for (const edge of this._edges) {
- const a = this._nodePositions[edge.source];
- const b = this._nodePositions[edge.target];
- if (!a || !b) continue;
-
- ctx.beginPath();
- ctx.moveTo(a.x, a.y);
- ctx.lineTo(b.x, b.y);
- ctx.strokeStyle = edge.relation === "wikilink" ? "var(--accent-color, #2563eb)" : "var(--text-muted, #888)";
- ctx.lineWidth = edge.relation === "wikilink" ? 2 : 1;
- ctx.setLineDash(edge.relation === "wikilink" ? [4, 4] : []);
- ctx.stroke();
- }
- ctx.setLineDash([]);
-
- // Draw nodes
- for (const node of this._nodes) {
- const p = this._nodePositions[node.id];
- if (!p) continue;
-
- const r = Math.max(5, Math.min(20, 6 + Math.sqrt(node.size || 100) / 100));
-
- // Node circle
- ctx.beginPath();
- ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
-
- switch (node.type) {
- case "directory":
- ctx.fillStyle = "#5b9bd5";
- break;
- case "file":
- ctx.fillStyle = (node.path || "").endsWith(".md") ? "#70ad47" : "#c0c0c0";
- break;
- default:
- ctx.fillStyle = "#ffc000";
- break;
- }
- ctx.fill();
- ctx.strokeStyle = "var(--bg-primary, #1e1e1e)";
- ctx.lineWidth = 1.5;
- ctx.stroke();
-
- // Node label
- const label = node.name.length > 20 ? node.name.slice(0, 18) + "..." : node.name;
- ctx.font = `${11 / this._zoom}px -apple-system, sans-serif`;
- ctx.fillStyle = "var(--text-primary, #ddd)";
- ctx.textAlign = "center";
- ctx.fillText(label, p.x, p.y + r + 12 / this._zoom);
- }
-
- ctx.restore();
- },
-
- _getNodeAt(screenX, screenY) {
- const x = (screenX - this._offsetX) / this._zoom;
- const y = (screenY - this._offsetY) / this._zoom;
-
- for (const node of this._nodes) {
- const p = this._nodePositions[node.id];
- if (!p) continue;
- const r = Math.max(5, Math.min(20, 6 + Math.sqrt(node.size || 100) / 100));
- const dx = x - p.x;
- const dy = y - p.y;
- if (dx * dx + dy * dy <= r * r + 100) {
- return { node, pos: p };
- }
- }
- return null;
- },
-
- _onMouseDown(e) {
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- const hit = this._getNodeAt(mx, my);
- if (hit) {
- this._dragging = true;
- this._dragNode = hit;
- this._canvas.style.cursor = "grabbing";
- } else {
- this._panning = true;
- this._panStartX = e.clientX - this._offsetX;
- this._panStartY = e.clientY - this._offsetY;
- this._canvas.style.cursor = "grabbing";
- }
- },
-
- _onMouseMove(e) {
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- if (this._dragging && this._dragNode) {
- this._dragNode.pos.x = (mx - this._offsetX) / this._zoom;
- this._dragNode.pos.y = (my - this._offsetY) / this._zoom;
- this._dragNode.pos.vx = 0;
- this._dragNode.pos.vy = 0;
- } else if (this._panning) {
- this._offsetX = e.clientX - this._panStartX;
- this._offsetY = e.clientY - this._panStartY;
- } else {
- const hit = this._getNodeAt(mx, my);
- this._canvas.style.cursor = hit ? "pointer" : "grab";
- this._canvas.title = hit ? `Ouvrir: ${hit.node.path || hit.node.name}` : "";
- }
- },
-
- _onMouseUp(e) {
- if (this._dragging && this._dragNode) {
- // Check if it was a click (not a drag)
- const rect = this._canvas.getBoundingClientRect();
- const node = this._dragNode.node;
- this._dragging = false;
- this._dragNode = null;
- this._canvas.style.cursor = "grab";
-
- // If it's a file, open it on click
- if (node.type === "file") {
- this.close();
- openFile(this._vault, node.path);
- } else if (node.type === "directory" || node.type === "vault") {
- // Expand into this directory
- this.close();
- this.open(this._vault, node.path, node.type);
- }
- }
- this._panning = false;
- },
-
- _onWheel(e) {
- e.preventDefault();
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
- const newZoom = Math.max(0.2, Math.min(3, this._zoom * zoomFactor));
-
- this._offsetX = mx - (mx - this._offsetX) * (newZoom / this._zoom);
- this._offsetY = my - (my - this._offsetY) * (newZoom / this._zoom);
- this._zoom = newZoom;
- },
-
- _onResize() {
- if (!this._canvas || !this._nodes.length) return;
- const w = this._canvas.parentElement.clientWidth;
- const h = this._canvas.parentElement.clientHeight;
- this._canvas.width = w * devicePixelRatio;
- this._canvas.height = h * devicePixelRatio;
- this._canvas.style.width = w + "px";
- this._canvas.style.height = h + "px";
- this._width = w;
- this._height = h;
- this._ctx.setTransform(1, 0, 0, 1, 0, 0);
- this._ctx.scale(devicePixelRatio, devicePixelRatio);
- },
- };
-
- // Init graph view event listeners after DOM ready
- function initGraphView() {
- const closeBtn = document.getElementById("graph-close");
- const zoomIn = document.getElementById("graph-zoom-in");
- const zoomOut = document.getElementById("graph-zoom-out");
- const reset = document.getElementById("graph-reset");
- const modal = document.getElementById("graph-modal");
- const canvas = document.getElementById("graph-canvas");
-
- if (closeBtn) closeBtn.addEventListener("click", () => GraphViewManager.close());
- if (modal) modal.addEventListener("click", (e) => { if (e.target === modal) GraphViewManager.close(); });
- if (zoomIn) zoomIn.addEventListener("click", () => { GraphViewManager._zoom = Math.min(3, GraphViewManager._zoom * 1.2); });
- if (zoomOut) zoomOut.addEventListener("click", () => { GraphViewManager._zoom = Math.max(0.2, GraphViewManager._zoom * 0.8); });
- if (reset) reset.addEventListener("click", () => {
- GraphViewManager._offsetX = 0;
- GraphViewManager._offsetY = 0;
- GraphViewManager._zoom = 1;
- });
-
- if (canvas) {
- canvas.addEventListener("mousedown", (e) => GraphViewManager._onMouseDown(e));
- canvas.addEventListener("mousemove", (e) => GraphViewManager._onMouseMove(e));
- canvas.addEventListener("mouseup", (e) => GraphViewManager._onMouseUp(e));
- canvas.addEventListener("mouseleave", () => {
- GraphViewManager._dragging = false;
- GraphViewManager._dragNode = null;
- GraphViewManager._panning = false;
- canvas.style.cursor = "grab";
- });
- canvas.addEventListener("wheel", (e) => GraphViewManager._onWheel(e), { passive: false });
- window.addEventListener("resize", () => GraphViewManager._onResize());
- }
-
- document.addEventListener("keydown", (e) => {
- if (modal && modal.classList.contains("active") && e.key === "Escape") {
- GraphViewManager.close();
- }
- });
- }
-
- // Add to init
- const _origInit = init;
- init = function() {
- _origInit();
- initGraphView();
- };
-
-})();
diff --git a/desktop/frontend-static/editor-poc.html b/desktop/frontend-static/editor-poc.html
deleted file mode 100644
index 18d7e53..0000000
--- a/desktop/frontend-static/editor-poc.html
+++ /dev/null
@@ -1,1998 +0,0 @@
-
-
-
-
-
-ObsiGate Editor v1 — POC
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🖼
Drop files here
Images, PDFs, documents
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- B
- I
- S
- </>
- 🔗
- ☀
-
- ⋯
-
-
-
-
-
-
- ✨ AI Assistant
- ×
-
-
-
Ask AI about your document. Use slash /ai or select text for AI actions.
-
-
- ✨ Improve overall style
- ➕ Add a conclusion
- 📝 Summarize document
-
-
-
- Send
-
-
-
-
-
-
-
- Type / for commands | Alt+\ autocomplete | Alt+I Insert
- Ctrl+J AI panel
- Ctrl+S save
- Ctrl+K link
- ✨ AI processing...
- ✨ AI ready
-
-
-
- 0 words
-
-
-
-
-
-
-
-
-
-
Forge Editor — Aide
-
Navigation
-
- / Menu de commandes (en debut de ligne)
- Alt+I Insertion rapide (Quick Insert)
- F1 Ce panneau d'aide
- Tab Indenter la ligne / element de liste
- Shift+Tab Desindenter
-
-
Formatage
-
- Ctrl+B **Gras**
- Ctrl+I *Italique*
- Ctrl+K Inserer un lien [texte](url)
- Selection + [ Envelopper en [lien](https://)
- Selection + ` Envelopper en `code`
-
-
Brackets & Auto-completion
-
- [ Auto-ferme [] avec curseur au milieu
- ( Auto-ferme () — si apres [texte], insere https://
- " Auto-ferme ""
- ` Auto-ferme ``
- Backspace Supprime la paire de brackets d'un coup
-
-
Fichier
-
- Ctrl+S Sauvegarder
- Clic sur le titre Renommer le fichier
- Ctrl+J Panneau AI
- Alt+\ Autocompletion intelligente
- Enter (liste) Continue la liste (-, *, 1.) et checkboxes
- Enter (liste vide) Termine la liste
-
-
Slash — Commandes
-
- /h1 /h2 /h3 Titres
- /bullet /numbered Listes
- /checklist Liste de taches
- /quote /code /divider Blocs
- /link /image /table Insertions
-
-
Fermer
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/README.md b/desktop/frontend-static/icons/README.md
deleted file mode 100644
index ec850f5..0000000
--- a/desktop/frontend-static/icons/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# ObsiGate PWA Icons
-
-## Generated Icons
-
-This directory contains PWA icons in SVG format.
-
-### Converting to PNG
-
-For production, convert these SVG files to PNG:
-
-**Using ImageMagick:**
-```bash
-for file in *.svg; do
- size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
- convert -background none -resize ${size}x${size} "$file" "${file%.svg}.png"
-done
-```
-
-**Using Inkscape:**
-```bash
-for file in *.svg; do
- size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
- inkscape "$file" --export-filename="${file%.svg}.png" --export-width=$size
-done
-```
-
-**Online tools:**
-- https://cloudconvert.com/svg-to-png
-- https://convertio.co/svg-png/
-
-### Icon Types
-
-- **Regular icons**: Standard app icons with rounded corners
-- **Maskable icons**: Icons with safe zone padding for adaptive icons
-- **Search icon**: Icon for the search shortcut
-
-### Sizes
-
-- 72x72, 96x96, 128x128, 144x144, 152x152: Mobile devices
-- 192x192: Android home screen
-- 384x384: High-res displays
-- 512x512: Splash screens and high-DPI displays
diff --git a/desktop/frontend-static/icons/icon-128x128.svg b/desktop/frontend-static/icons/icon-128x128.svg
deleted file mode 100644
index d044b04..0000000
--- a/desktop/frontend-static/icons/icon-128x128.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-144x144.svg b/desktop/frontend-static/icons/icon-144x144.svg
deleted file mode 100644
index d48435d..0000000
--- a/desktop/frontend-static/icons/icon-144x144.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-152x152.svg b/desktop/frontend-static/icons/icon-152x152.svg
deleted file mode 100644
index 54c763b..0000000
--- a/desktop/frontend-static/icons/icon-152x152.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-192x192-maskable.svg b/desktop/frontend-static/icons/icon-192x192-maskable.svg
deleted file mode 100644
index 438889a..0000000
--- a/desktop/frontend-static/icons/icon-192x192-maskable.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-192x192.svg b/desktop/frontend-static/icons/icon-192x192.svg
deleted file mode 100644
index 069f0dc..0000000
--- a/desktop/frontend-static/icons/icon-192x192.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-384x384.svg b/desktop/frontend-static/icons/icon-384x384.svg
deleted file mode 100644
index 96e783c..0000000
--- a/desktop/frontend-static/icons/icon-384x384.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-512x512-maskable.svg b/desktop/frontend-static/icons/icon-512x512-maskable.svg
deleted file mode 100644
index f2217fd..0000000
--- a/desktop/frontend-static/icons/icon-512x512-maskable.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-512x512.svg b/desktop/frontend-static/icons/icon-512x512.svg
deleted file mode 100644
index dd9a6c6..0000000
--- a/desktop/frontend-static/icons/icon-512x512.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-72x72.svg b/desktop/frontend-static/icons/icon-72x72.svg
deleted file mode 100644
index 3c67e91..0000000
--- a/desktop/frontend-static/icons/icon-72x72.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/icon-96x96.svg b/desktop/frontend-static/icons/icon-96x96.svg
deleted file mode 100644
index 9831f65..0000000
--- a/desktop/frontend-static/icons/icon-96x96.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/icons/search-96x96.svg b/desktop/frontend-static/icons/search-96x96.svg
deleted file mode 100644
index b3f0ee0..0000000
--- a/desktop/frontend-static/icons/search-96x96.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/desktop/frontend-static/index.html b/desktop/frontend-static/index.html
deleted file mode 100644
index 4b810f9..0000000
--- a/desktop/frontend-static/index.html
+++ /dev/null
@@ -1,4827 +0,0 @@
-
-
-
-
-
- ObsiGate
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 📂 Toutes les vaults
-
-
-
-
-
-
-
-
-
-
-
-
- Statistiques
-
-
-
- Bookmarks
-
-
-
- Récents
-
-
-
- Partagés
-
-
-
-
-
-
-
-
-
-
-
-
- Ctrl+Shift+Space
- Palette de commandes
-
-
- Ctrl+Alt+Space
- Palette de fichiers (nav.
- rapide)
-
-
- Ctrl+K
- Focus barre de recherche
-
-
- /
- Recherche rapide (hors champ
- texte)
-
-
- Ctrl+S
- Sauvegarder le fichier
- ouvert
-
-
- Ctrl+F
- Rechercher dans le
- document
-
-
- Esc
- Fermer l'editeur / modale
-
-
- Ctrl+J
- Completion IA inline (dans
- l'editeur)
-
-
- Alt+I
- Insertion rapide (editeur
- Forge)
-
-
- F1
- Aide de l'editeur Forge
-
-
-
-
- Survolez un document avec
- Ctrl pour un
- apercu rapide. Cliquez sur
- Forge dans la
- barre d'outils pour l'editeur
- avance.
-
-
-
-
-
-
-
-
-
-
-
Aucun bookmark
-
- Épinglez des fichiers pour les retrouver
- ici.
-
-
-
-
-
-
-
-
-
-
-
-
Aucun fichier récent
-
- Ouvrez un fichier pour le voir
- apparaître ici
-
-
-
-
-
-
-
-
-
-
Aucun document partagé
-
- Partagez un document pour le voir
- apparaître ici
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- X
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sauvegarder
-
-
- Forcer réindexation
-
-
- Réinitialiser
-
-
-
-
-
-
-
-
- Définissez les patterns de tags à masquer
- dans la sidebar. Vous pouvez utiliser des
- wildcards pour cibler les tags de template.
-
-
-
-
-
-
-
- Ajouter
-
-
-
-
- Regex :
-
-
-
-
-
-
-
-
-
- 🗂️ Fichiers cachés
-
- Contrôlez l'affichage des fichiers/dossiers
- cachés (commençant par .) par
- vault.
-
-
- i️ Note : Tous les fichiers
- sont toujours indexés et cherchables. Ce
- paramètre contrôle uniquement leur
- visibilité dans l'interface.
-
-
-
-
-
-
-
-
- 💾 Sauvegarder
-
-
-
-
-
-
- Diagnostics
-
- Statistiques de l'index et du moteur de
- recherche.
-
-
-
- Rafraîchir
-
-
-
-
-
-
-
-
- 🎨 Thèmes
-
- Choisissez un thème visuel. Chaque thème
- offre un mode sombre et clair.
-
-
-
-
-
-
- 👤 Profil utilisateur
-
- Préférences personnelles pour personnaliser
- l'expérience ObsiGate.
-
-
-
-
-
-
-
-
-
-
-
-
- 📤 Partages publics
-
- Liens de partage publics pour des documents
- (lecture seule, sans authentification).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ✕
-
-
-
-
-
-
-
-
Guide officiel
-
📘 Bienvenue dans ObsiGate
-
- ObsiGate est votre porte d'entrée web vers
- vos vaults Obsidian. Accédez, naviguez et
- recherchez dans toutes vos notes depuis
- n'importe quel appareil via une interface
- moderne et responsive.
-
-
- ⚡ Navigation rapide
- 🗂️ Multi-vault
- 🏷️ Tags intelligents
- 📝 Lecture et édition
- 🔍 Recherche TF-IDF
-
-
-
-
- Expérience
- Simple
-
-
- Recherche
- Instantanée
-
-
- Accès
- Multi-vault
-
-
-
-
-
- 🧭 Interface utilisateur
-
- En-tête
-
- La barre supérieure contient les éléments
- essentiels :
-
-
-
- Bouton menu hamburger
- : obile) : Affiche/masque la sidebar
-
-
- Bouton toggle sidebar (desktop) : Masque la sidebar pour plus
- d'espace
-
-
- Logo ObsiGate : Retour à
- l'accueil
-
-
- Barre de recherche :
- Recherche globale avec autocomplétion
-
-
- Indicateur de vault :
- Affiche le contexte actif (All, vault1,
- etc.)
-
-
- Menu Options : Accès aux
- paramètres, thème, aide
-
-
-
- Sidebar (barre latérale)
- La sidebar est divisée en deux onglets :
-
-
- Vaults : Arborescence des
- fichiers et dossiers
-
-
- Tags : Nuage de tags
- cliquables
-
-
- Fonctionnalités de la sidebar :
-
-
- Filtrage : Barre de
- recherche pour filtrer fichiers/tags
-
-
- Sélection rapide : Dropdown
- pour changer de vault
-
-
- Redimensionnement : Glissez
- le bord droit pour ajuster la largeur
-
-
- Masquage : Cliquez sur
- l'icône sidebar dans le header
-
-
-
- Zone de contenu
- La zone centrale affiche :
-
-
- Breadcrumb : Chemin du
- fichier (cliquable pour repositionner la
- sidebar)
-
-
- Titre et tags : Métadonnées
- du fichier
-
-
- Boutons d'action : Copier,
- Source, Télécharger, Éditer, Ouvrir dans
- nouvelle fenêtre
-
-
- Contenu rendu : Markdown
- avec wikilinks et images
-
-
-
-
-
- 🗺️ Navigation
-
- Arborescence des vaults
-
- Cliquez sur un vault pour l'ouvrir et voir son
- contenu. Les dossiers affichent un chevron et un
- badge avec le nombre d'éléments.
-
-
-
- Développer/Réduire :
- Cliquez sur un dossier
-
-
- Ouvrir un fichier : Cliquez
- sur le nom du fichier
-
-
- Icônes : Chaque type de
- fichier a son icône (markdown, code, etc.)
-
-
- Filtrage de la sidebar
-
- Utilisez la barre de filtrage en haut de la
- sidebar pour rechercher rapidement :
-
-
-
- Recherche instantanée :
- Tapez pour filtrer fichiers et dossiers
-
-
- Case-sensitive : Bouton
- "Aa" pour respecter la casse
-
-
- Effacer : Bouton "×" pour
- réinitialiser le filtre
-
- Résultats groupés :
- Affichage par vault avec chemins complets
-
-
-
- Breadcrumb et repositionnement
-
- Le chemin affiché au-dessus du contenu est
- interactif :
-
-
-
- Cliquez sur le vault :
- Focus sur le vault dans la sidebar
-
-
- Cliquez sur un segment :
- Navigation vers ce dossier
-
-
- Auto-scroll : La sidebar se
- positionne automatiquement
-
-
-
- Sélection de vault
-
- Changez de contexte pour filtrer l'affichage :
-
-
-
- Menu header : Dropdown
- "Vault" dans le menu Options
-
-
- Sidebar dropdown :
- Sélection rapide en haut de l'onglet Vaults
-
- Tous les vaults : Vue
- globale de tous vos contenus
-
-
- Vault spécifique : Focus
- sur un seul vault
-
-
-
-
-
- 📑 Gestion des onglets
-
- ObsiGate supporte l'ouverture de plusieurs
- fichiers simultanément dans des onglets.
-
-
- Ouverture d'onglets
-
-
- Simple clic : Ouvre le
- fichier dans un onglet temporaire (aperçu,
- italique)
-
- Double clic : Ouvre le
- fichier dans un onglet persistant
-
-
- Navigation récente :
- L'onglet "Récent" dans la sidebar liste vos
- derniers fichiers
-
-
- Navigation entre onglets
-
-
- Clic sur un onglet : Active
- l'onglet
-
-
- Ctrl+Tab : Onglet suivant
-
-
- Ctrl+Shift+Tab : Onglet
- précédent
-
- Ctrl+W : Fermer l'onglet
- actif
-
-
- Bouton ✕ : Ferme l'onglet
- (sauf s'il est seul)
-
-
-
- Fonctionnalités
-
-
- Barre d'onglets : Affichage
- horizontal avec le nom du fichier et son
- icône
-
- Onglet actif : Surligné en
- couleur d'accentuation
-
- Prévisualisation : Un
- onglet temporaire (italique) est remplacé
- par le prochain clic
-
- Onglets persistants :
- Restent ouverts jusqu'à fermeture manuelle
-
-
-
-
- 🔍 Recherche
-
- Recherche simple
-
- Tapez dans la barre de recherche en haut pour
- lancer une recherche fulltext :
-
-
-
- Recherche dans tout :
- Titre, contenu, tags, chemins
-
-
- Scoring intelligent :
- Résultats classés par pertinence TF-IDF
-
- Snippets surlignés :
- Extraits avec termes en surbrillance
-
-
- Pagination : Navigation par
- pages de 50 résultats
-
-
- Opérateurs avancés
-
- Utilisez des opérateurs pour affiner votre
- recherche :
-
-
-
- tag:docker ou
- #docker : Filtrer par tag
-
-
- vault:IT : Rechercher dans un
- vault spécifique
-
-
- title:kubernetes : Chercher
- dans les titres uniquement
-
-
- path:recettes/soupes : Filtrer
- par chemin
-
-
- ext:md : Filtrer par type de
- fichier
-
-
- "phrase exacte" : Recherche de
- phrase entre guillemets
-
-
-
- Exemples :
- ext:sh recherche dans les scripts
- bash, ext:py dans les scripts
- Python, ext:md dans les fichiers
- Markdown.
-
-
- Exemple combiné :
- tag:linux vault:IT ext:md serveur web
- recherche "serveur web" dans les fichiers
- Markdown du vault IT avec le tag linux.
-
-
- Autocomplétion
-
- L'autocomplétion vous aide à trouver rapidement
- :
-
-
-
- Historique : Les 50
- dernières recherches (localStorage)
-
- Fichiers : Suggestions de
- titres de fichiers
-
-
- Tags : Suggestions de tags
- avec compteurs
-
-
- Navigation clavier : ↑/↓
- pour naviguer, Enter pour sélectionner
-
-
- Chips de filtres actifs
-
- Les opérateurs utilisés s'affichent sous forme
- de chips colorés :
-
-
-
- Visualisation : Voir d'un
- coup d'œil les filtres actifs
-
-
- Suppression : Cliquez sur
- "×" pour retirer un filtre
-
-
- Couleurs : Chaque type
- d'opérateur a sa couleur
-
-
- Facettes et tri
-
- Les résultats incluent des facettes pour affiner
- :
-
-
-
- Facettes tags : Compteurs
- par tag dans les résultats
-
- Facettes vaults : Compteurs
- par vault
-
-
- Tri par pertinence : Score
- TF-IDF (défaut)
-
- Tri par date : Dernière
- modification
-
-
-
-
-
-
- 📄 Fichiers
-
- Visualisation
- Les fichiers markdown sont rendus avec :
-
-
- Wikilinks cliquables :
- [[lien]] et
- [[lien|texte]]
-
-
- Images Obsidian : Support
- de ![[image.png]]
-
-
- Syntax highlighting :
- Coloration des blocs de code
-
-
- Tables, listes, quotes :
- Rendu complet du markdown
-
-
-
- Actions sur fichiers
- Boutons disponibles au-dessus du contenu :
-
-
- Copier : Copie le contenu
- source dans le presse-papiers
-
-
- Source : Affiche/masque le
- markdown brut
-
-
- Télécharger : Télécharge le
- fichier
-
- Éditer : Ouvre l'éditeur
- CodeMirror
-
- Ouvrir dans nouvelle fenêtre : Ouvre le fichier dans un nouvel onglet
-
-
-
- Édition avec CodeMirror 6
- L'éditeur intégré offre :
-
-
- Coloration syntaxique :
- Support de nombreux langages
-
-
- Numéros de ligne :
- Navigation facile
-
-
- Autocomplétion :
- Suggestions contextuelles
-
-
- Recherche : Ctrl+F pour
- chercher dans le fichier
-
-
- Sauvegarde : Bouton ✓ pour
- enregistrer
-
-
- Suppression : Bouton
- corbeille pour supprimer
-
-
-
- Types de fichiers supportés
- ObsiGate indexe et affiche :
-
-
- Markdown : .md (rendu
- complet)
-
-
- Code : .py, .js, .ts, .jsx,
- .tsx, .java, .c, .cpp, .rs, .go, etc.
-
-
- Config : .json, .yaml,
- .yml, .toml, .xml, .ini, .env
-
-
- Scripts : .sh, .bash, .ps1,
- .bat
-
-
- Web : .html, .css, .scss
-
-
- Autres : .txt, .log, .sql,
- .csv
-
-
-
-
-
-
- ✏️ Édition avancée
-
- Éditeur CodeMirror 6
-
- L'éditeur intégré offre une expérience complète
- d'édition :
-
-
-
- Coloration syntaxique :
- Python, JS, HTML, CSS, Java, C++, Rust, Go
- et bien d'autres
-
-
- Numéros de ligne :
- Navigation facilitée dans les fichiers longs
-
- Find in page : Ctrl+F pour
- chercher dans le fichier (casse, regex, mot
- entier)
-
-
- Sauvegarde automatique :
- Auto-save silencieux après 2s d'inactivité,
- flash vert de confirmation
-
- Bouton ✓ : Sauvegarde
- manuelle
-
-
- Bouton 🗑️ : Suppression du
- fichier
-
-
- Barre d'outils AI :
- Complétion, réécriture, traduction (voir
- section 🤖 IA )
-
-
-
-
-
-
- 🗺️ Vue Graphe
-
- La vue graphe offre une visualisation
- interactive des connexions entre vos notes via
- les wikilinks.
-
-
- Ouverture
-
-
- Menu contextuel : Clic
- droit sur un vault ou dossier → Vue
- Graphique
-
-
- Bouton dédié : Dans la
- barre d'outils (le cas échéant)
-
-
- Interaction
-
-
- Zoom/Pan : Molette pour
- zoomer, cliquer-glisser pour se déplacer
-
- Slider de profondeur :
- Contrôle la profondeur d'exploration (0-3)
-
- Filtre par tag : N'affiche
- que les nœuds avec un tag spécifique
-
- Filtre par type :
- Inclut/exclut fichiers, dossiers, markdown,
- autres
-
-
- Recherche visuelle : Barre
- de recherche pour surligner des nœuds
-
-
- Mode focus : Centre la vue
- sur un nœud spécifique
-
- Navigation ← → ↑ :
- Historique de navigation entre les nœuds
-
-
-
- Affichage
-
-
- Nœuds : Fichiers (carrés)
- et dossiers (cercles) avec icônes
-
- Arêtes : Wikilinks sortants
- (bleu) et backlinks entrants (vert)
-
-
- Panneau d'info :
- Métadonnées du nœud (tags, liens, chemin)
-
- Ctrl+survol : Aperçu du
- contenu de la note sans naviguer
-
- Plein écran : Bouton pour
- agrandir la vue
-
-
- Export PNG : Sauvegardez le
- graphe en image
-
-
-
-
-
- La vue utilise l'algorithme Barnes-Hut pour
- calculer la disposition en O(n log n),
- garantissant des performances fluides même avec
- des centaines de nœuds.
-
-
-
-
- 🎨 Personnalisation
-
- Thème clair/sombre
- Basculez entre les thèmes :
-
-
- Menu Options : Cliquez sur
- "Thème" dans le menu
-
- Raccourci :
- Ctrl+T
-
-
- Persistance : Votre choix
- est sauvegardé en localStorage
-
- Highlight.js : Les blocs de
- code s'adaptent au thème
-
-
- Redimensionnement de la sidebar
-
- Sur desktop, ajustez la largeur de la sidebar :
-
-
-
- Handle de redimensionnement
- : Bordure droite de la sidebar
-
-
- Glisser-déposer : Cliquez
- et glissez pour ajuster
-
-
- Limites : Min 200px, Max
- 500px
-
-
- Persistance : La largeur
- est sauvegardée
-
-
- Configuration avancée
-
- Le modal Configurations (menu Options) permet de
- :
-
-
-
- Paramètres de recherche :
- Délai debounce, résultats par page, timeout
-
- Paramètres backend :
- Workers, boosts TF-IDF, expansions préfixe
-
- Filtrage de tags : Patterns
- pour masquer les tags template
-
-
- Diagnostics : Stats index,
- mémoire, moteur de recherche
-
- Réindexation : Forcer un
- rescan des vaults
-
-
-
-
-
-
- ⌨️ Palette de commandes
-
- La palette de commandes permet d'accéder
- rapidement aux fichiers et aux actions sans
- utiliser la souris.
-
-
- Ouverture
-
-
- Ctrl+Shift+Espace : Palette
- de fichiers (recherche par titre)
-
-
- Ctrl+Alt+Espace : Palette
- de commandes (liste d'actions)
-
-
- Tab : Bascule entre mode
- fichiers et mode commandes
-
-
-
- Mode fichiers
-
- Tapez le nom d'un fichier pour le trouver
- rapidement. La recherche utilise l'API
- /api/suggest pour des suggestions
- instantanées. Appuyez sur Enter pour
- ouvrir le fichier sélectionné.
-
-
- Mode commandes
-
- Tapez > (ou Ctrl+Alt+Espace)
- pour voir la liste des actions disponibles :
-
-
-
- 🏠 Accueil : Retour à
- l'accueil
-
-
- ✏️ Éditer fichier courant :
- Ouvre l'éditeur sur l'onglet actif
-
-
- 🪟 Pop-out : Ouvre le
- document dans une fenêtre détachée
-
-
- 📄 Créer fichier markdown :
- Crée un nouveau fichier .md et l'ouvre dans
- l'éditeur
-
-
- 🗑️
- Supprimer fichier courant :
- Supprime le fichier de l'onglet actif
-
-
- 📁 Créer répertoire : Crée
- un nouveau dossier
-
-
- 🗑️ Supprimer répertoire :
- Supprime un dossier et son contenu
-
-
- 🌓 Changer le thème :
- Bascule clair/sombre
-
-
- 🔄 Réindexer : Force la
- réindexation complète
-
-
- ❓ Aide : Ouvre ce guide
-
-
- ⚙️ Configuration : Ouvre
- les paramètres
-
-
-
- Navigation
-
-
- ↑ ↓ : Se déplacer dans
- la liste
-
- Enter : Ouvrir/sélectionner
- Esc : Fermer la palette
-
- Tab : Basculer entre fichiers et
- commandes
-
-
-
-
-
- ⌨️ Raccourcis clavier
-
- Navigation
-
-
- Ctrl+K ou Cmd+K :
- Focaliser la barre de recherche
-
-
- / : Focaliser la recherche
- (hors champ texte)
-
-
- Escape : Fermer les suggestions
- / quitter la recherche
-
-
-
- Recherche et autocomplétion
-
-
- ↑ / ↓ : Naviguer
- dans les suggestions
-
-
- Enter : Sélectionner la
- suggestion active ou lancer la recherche
-
-
- Escape : Fermer le dropdown de
- suggestions
-
-
-
- Interface
-
-
- Ctrl+T : Basculer le thème
- clair/sombre
-
-
- Ctrl+Shift+Espace : Palette de
- fichiers
-
-
- Ctrl+Alt+Espace : Palette de
- commandes
-
-
- Escape : Fermer la palette /
- les suggestions
-
-
-
- Onglets
-
-
- Ctrl+W : Fermer l'onglet actif
-
- Ctrl+Tab : Onglet suivant
-
- Ctrl+Shift+Tab : Onglet
- précédent
-
-
-
- Éditeur CodeMirror
-
-
- Ctrl+F : Rechercher dans le
- fichier
-
- Ctrl+Z : Annuler
- Ctrl+Y : Refaire
-
- Ctrl+S : Sauvegarder (si
- configuré)
-
-
-
-
-
- 🤖 Intelligence Artificielle dans l'Éditeur
-
- ObsiGate intègre un assistant IA directement
- dans l'éditeur de texte. Pour l'activer,
- configurez au moins un provider AI dans le
- fichier .env (DeepSeek, OpenRouter
- ou Gemini).
-
-
- Activation
-
-
- Ajoutez votre clé API dans
- .env :
- DEEPSEEK_API_KEY=sk-...
-
-
- Redémarrez le conteneur :
- docker compose down && docker compose
- up -d --build
-
-
- Ouvrez un fichier en mode Édition (bouton ✏️
- Éditer)
-
-
- La barre d'outils AI apparaît sous le titre
- de l'éditeur
-
-
-
- Barre d'outils AI
-
- La barre d'outils contient les boutons suivants
- :
-
-
-
- ✦ AI — Complétion inline
- ( Ctrl+J ). Complète
- automatiquement le texte là où se trouve le
- curseur.
-
-
- Éditer ▾ — Menu de
- modification du texte sélectionné :
-
- 🪄
- Improve writing :
- Améliore la qualité, la clarté et le
- flux du texte
-
-
- 🔤
- Fix spelling & grammar
- : Corrige les fautes d'orthographe
- et de grammaire
-
-
- 📏 Make shorter :
- Rend le texte plus concis tout en
- préservant l'information
-
-
- 📐 Make longer :
- Ajoute des détails, exemples ou
- explications
-
-
- 📋
- Simplify language :
- Simplifie le langage, évite le
- jargon
-
-
-
-
- Ton ▾ — Change le ton du
- texte :
-
-
- 💼 Professional :
- Ton formel et professionnel
-
-
- 💬 Casual : Ton
- décontracté et conversationnel
-
-
-
-
- Traduire ▾ — Traduit le
- texte sélectionné vers une autre langue :
-
- 🇬🇧 English · 🇨🇳 Chinese · 🇯🇵
- Japanese · 🇩🇪 German · 🇫🇷 French ·
- 🇪🇸 Spanish
-
-
-
-
- Générer ▾ — Génère du
- contenu à partir de la sélection :
-
- ℹ️ Explain this :
- Explique le texte sélectionné de
- façon claire
-
-
- 📝 Summarize :
- Résume le texte de façon concise
-
-
- ✏️
- Continue writing :
- Continue l'écriture à partir du
- texte
-
-
-
-
- 💬 Réécrire — Réécrit le
- texte selon vos instructions personnalisées.
- Une boîte de dialogue vous demande votre
- instruction.
-
- 🧰 Boîte ▾ — Outils de
- conversion :
-
-
- 📋
- Convert to list :
- Transforme le texte en liste
- Markdown
-
-
- 📊
- Convert to table :
- Transforme en tableau Markdown
-
-
- ⚙️
- Generate frontmatter
- : Génère un bloc YAML frontmatter
-
-
- 🔷
- Convert to canvas :
- Convertit en diagramme Mermaid.js
-
-
-
-
-
- Indicateurs visuels
-
-
- Pendant une requête AI, un toast
- ⏳ AI: traitement en cours...
- s'affiche
-
-
- Une fois terminé,
- AI: texte traité ✓ confirme
- le succès
-
-
- En cas d'erreur de clé API :
- AI: clé API invalide
-
-
-
- Sauvegarde automatique
-
- L'éditeur sauvegarde automatiquement après 2
- secondes d'inactivité. Le bouton de sauvegarde
- flashe en vert brièvement pour
- confirmer.
-
-
- Providers supportés
-
-
- Provider
- Variable .env
- Modèle par défaut
-
-
- DeepSeek
- DEEPSEEK_API_KEY
- deepseek-chat
-
-
- OpenRouter
- OPENROUTER_API_KEY
- openai/gpt-4o-mini
-
-
- Gemini
- GEMINI_API_KEY
- gemini-2.0-flash
-
-
-
- Pour changer de provider par défaut :
- AI_DEFAULT_PROVIDER=openrouter
-
-
-
-
-
- 🔗 Partage et Webhooks
-
- Publication publique
-
- ObsiGate permet de partager des documents via un
- lien public temporaire.
-
-
-
- Bouton Partager : Dans les
- actions du fichier, génère un lien unique
-
- Token unique : 64
- caractères hexadécimaux, impossible à
- deviner
-
- Expiration configurable :
- Durée de vie en heures
-
- Lecture seule : La vue
- publique n'affiche que le contenu sans
- édition
-
- Gestion des liens : Liste
- et révocation dans les Configurations
-
- Export PDF : Téléchargement
- du document partagé en PDF via /s/{token}/pdf
-
-
-
-
-
-
- 🪝 Webhooks
-
- Les webhooks permettent à ObsiGate de
- notifier automatiquement des services
- externes
- lorsque des fichiers ou dossiers sont modifiés.
- Chaque événement déclenche un
- POST HTTP vers l'URL de votre
- choix, avec un payload JSON détaillé.
-
-
- Cas d'usage
-
-
- Automatisation CI/CD :
- Déclencher un build quand une note de
- procédure est mise à jour
-
- Synchronisation : Copier
- les nouveaux fichiers vers un autre système
- (SharePoint, Google Drive)
-
- Notifications : Alerter un
- canal Slack/Discord quand un document est
- créé
-
- Audit externe : Logger
- toutes les modifications dans une base de
- données externe
-
- Déploiement : Publier un
- article de blog quand un fichier est marqué publish: true
-
-
-
- Événements supportés
-
-
-
-
- Événement
-
-
- Déclencheur
-
-
- Données
-
-
-
-
- file_created
-
-
- Création via l'API ou la palette
-
-
- vault,
- path
-
-
-
-
- file_deleted
-
-
- Suppression via l'API ou l'éditeur
-
-
- vault,
- path
-
-
-
-
- file_renamed
-
-
- Renommage via l'API
-
-
- vault,
- old_path,
- new_path
-
-
-
-
- directory_created
-
-
- Création de dossier via l'API
-
-
- vault,
- path
-
-
-
-
- directory_deleted
-
-
- Suppression de dossier via l'API
-
-
- vault,
- path
-
-
-
-
- directory_renamed
-
-
- Renommage de dossier via l'API
-
-
- vault,
- old_path,
- new_path
-
-
-
-
-
- Note : l'événement
- file_modified est réservé pour
- usage futur. Les modifications de contenu
- via l'éditeur ne déclenchent pas encore de
- webhook.
-
-
- Format du payload
-
- Chaque webhook reçoit un POST avec
- Content-Type: application/json :
-
- {
- "event": "file_created",
- "timestamp": "2026-06-03T14:30:00.123456+00:00",
- "data": {
- "vault": "IT",
- "path": "procedures/deploiement.md"
- }
-}
-
- Sécurité : signature HMAC-SHA256
-
- Si vous configurez un secret, chaque requête
- inclut un header
- X-ObsiGate-Signature :
-
- X-ObsiGate-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
-
- Pour vérifier côté récepteur : calculez
- HMAC-SHA256(corps_brut, secret) et
- comparez. Le header
- X-ObsiGate-Event contient aussi le
- type d'événement pour un routage rapide.
-
-
- Configuration
-
- Les webhooks se gèrent dans le modal
- Configurations (⚙️ menu
- Options), section Webhooks. Réservé aux
- administrateurs.
-
-
-
- Allez dans ⚙️ Configurations → onglet
- Webhooks
-
-
- Remplissez :
- Nom (descriptif),
- URL (endpoint cible),
- Secret (optionnel)
-
- Cochez les événements à écouter
- Cliquez Ajouter
-
-
- Les webhooks peuvent être activés/désactivés,
- modifiés ou supprimés à tout moment. Le
- last_fired_at indique la dernière
- exécution.
-
-
- Exemples concrets
-
- Récepteur Python (Flask)
- from flask import Flask, request\nimport hmac, hashlib\n\napp = Flask(__name__)\nWEBHOOK_SECRET = "mon-secret" # même que dans ObsiGate\n\n@app.route("/webhook", methods=["POST"])\ndef webhook():\n # Vérifier la signature\n signature = request.headers.get("X-ObsiGate-Signature", "")\n body = request.get_data()\n expected = "sha256=" + hmac.new(\n WEBHOOK_SECRET.encode(), body, hashlib.sha256\n ).hexdigest()\n if not hmac.compare_digest(signature, expected):\n return "Invalid signature", 403\n\n payload = request.get_json()\n print(f"Événement: {payload['event']}")\n print(f"Fichier: {payload['data']['vault']}/{payload['data']['path']}")\n return "OK", 200\n\napp.run(port=5000)
-
- n8n / Make (no-code)
-
-
- Créez un
- Webhook Trigger dans n8n ou
- Make
-
-
- Copiez l'URL générée (ex:
- https://n8n.monserveur.com/webhook/abc123)
-
-
- Dans ObsiGate, ajoutez un webhook pointant
- vers cette URL
-
- Sélectionnez les événements souhaités
-
- Le workflow se déclenche automatiquement à
- chaque événement
-
-
-
- Notification Discord
-
- Utilisez un webhook relais (n8n, Make, ou un
- script) pour transformer le payload ObsiGate en
- message Discord :
-
- # Exemple avec curl + jq dans un script bash
-WEBHOOK_URL="https://discord.com/api/webhooks/..."
-OBSIGATE_SECRET="mon-secret"
-
-# Lire le payload depuis stdin
-read -r payload
-event=$(echo "$payload" | jq -r '.event')
-path=$(echo "$payload" | jq -r '.data.path')
-
-curl -H "Content-Type: application/json" -d "{
- \"content\": \"📝 **$event** — $path\"
-}" "$WEBHOOK_URL"
-
- Tester avec curl
- # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)
-curl -X POST https://votre-serveur.com/webhook \
- -H "Content-Type: application/json" \
- -H "X-ObsiGate-Event: file_created" \
- -H "X-ObsiGate-Signature: sha256=$(echo -n '{"event":"file_created",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)" \
- -d '{"event":"file_created","timestamp":"2026-06-03T14:30:00Z","data":{"vault":"IT","path":"notes/test.md"}}'
-
- Limitations actuelles
-
-
- Timeout de 5 secondes par
- requête — les webhooks sont "fire and
- forget" (pas de retry)
-
-
- Les modifications détectées par le watcher
- filesystem (watchdog)
- ne déclenchent pas de
- webhooks — seules les opérations via l'API
- REST sont prises en compte
-
-
- file_modified n'est pas encore
- dispatché (réservé pour version future)
-
- Pas de retry automatique en cas d'échec
-
- Pas de filtrage par vault — un webhook
- écoute toutes les vaults
-
-
-
-
- 💾 Sauvegardes et Audits
-
- Backup automatique
-
- ObsiGate crée automatiquement des sauvegardes
- avant chaque modification destructive.
-
-
-
- Avant écriture : Sauvegarde
- du contenu original dans
- .obsigate-backup/
-
-
- Timestamp : Chaque backup
- est horodaté
-
- Répertoire dédié :
- .obsigate-backup/ dans le
- répertoire de la vault
-
-
-
- Audit log
-
- Toutes les opérations sensibles sont tracées
- dans un journal.
-
-
-
- Journal : Fichier
- data/audit.log au format JSON
- lines
-
-
- Événements tracés :
- Écritures, suppressions, changements de
- configuration
-
- Rotation : Rotation
- automatique à 10 Mo
-
-
-
-
-
- 🛡️ Sécurité
-
- Authentification
-
-
- JWT + Argon2id : Tokens JWT
- avec mots de passe hachés Argon2id
-
- Contrôle d'accès par vault
- : Chaque utilisateur voit uniquement ses
- vaults
-
-
- Refresh tokens : Cookies
- HttpOnly avec rotation automatique
-
-
- Rate limiting : 10
- tentatives / 15 min par IP + verrouillage
- compte
-
-
- Compte admin : Créé
- automatiquement au premier démarrage
-
-
- Protection
-
-
- Secret redactor : Masque
- JWT, clés API, tokens GitHub dans les
- aperçus
-
- Path traversal : Validation
- des chemins contre les attaques
- ../
-
-
- Headers CSP :
- Content-Security-Policy, X-Frame-Options,
- XSS-Protection
-
-
- Atomic writes : Écriture
- via fichier temporaire + renommage atomique
-
- Non-root Docker : Conteneur
- tourne avec UID 1000
-
-
-
-
-
- 💡 Astuces et bonnes pratiques
-
- Workflow recommandé
-
-
- Sélectionnez un contexte :
- Choisissez un vault pour réduire le bruit
-
- Explorez l'arborescence :
- Naviguez dans les dossiers
-
-
- Utilisez les tags : Filtrez
- par tags pour affiner
-
-
- Recherchez intelligemment :
- Combinez opérateurs et texte libre
-
- Profitez de l'historique :
- Réutilisez vos recherches fréquentes
-
-
- Optimisations
-
-
- Masquez la sidebar : Plus
- d'espace pour le contenu
-
-
- Utilisez le filtrage sidebar
- : Plus rapide que la recherche globale pour
- trouver un fichier
-
-
- Favorisez les opérateurs :
- tag: et
- vault: accélèrent la recherche
-
-
- Breadcrumb cliquable :
- Repositionnez rapidement la sidebar
-
-
-
- Trucs et astuces
-
-
- Navigation visuelle : Le
- breadcrumb repositionne automatiquement la
- sidebar
-
-
- Gestion de l'espace :
- Réduisez les sections Vaults/Tags selon vos
- besoins
-
- Confort de lecture :
- Ajustez la largeur de la sidebar
-
-
- Mobile-friendly : Utilisez
- le bouton hamburger sur mobile
-
-
- Wikilinks : Naviguez entre
- notes comme dans Obsidian
-
-
- Images : Support complet
- des syntaxes Obsidian
-
-
-
- Cas d'usage avancés
-
-
- Recherche multi-critères :
- tag:docker tag:linux vault:IT
- nginx
-
-
- Exploration par tags :
- Cliquez sur un tag dans une note pour voir
- toutes les notes similaires
-
-
- Édition rapide : Ouvrez
- l'éditeur, modifiez, sauvegardez
-
- Export : Téléchargez des
- fichiers pour backup ou partage
-
- Multi-fenêtres : Ouvrez
- plusieurs notes dans des onglets séparés
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0 occurrence
-
-
-
-
-
-
-
-
-
-
- Aa
-
-
- abc
-
-
- .*
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- settings.search
-
-
-
- Recherche
-
-
-
- Commandes
-
-
-
- 0
- Onglets
-
-
-
-
-
-
-
-
- 0/0
- ↑
- ↓
-
- Aa
- wd
- .*
-
-
-
-
-
-
-
-
- Garder ma version
- Garder version serveur
-
-
-
-
-
-
-
-
×
-
-
-
-
-
-
-
-
-
-
-
ObsiGate
-
Vos notes Obsidian, simplement accessibles.
-
v2.0.0-dev
-
-
-
-
- —
- Fichiers
-
-
- —
- Vaults
-
-
- 285
- Tests
-
-
-
-
Construit avec
-
- 🐍 FastAPI
- ⚡ vanilla JS
- 🎨 CSS custom
- 🐳 Docker
- 🔍 TF-IDF
- 📝 CodeMirror 6
- 🧜 Mermaid.js
- 🧠 Argon2id
- 🔄 Watchdog
- 📊 WeasyPrint
- 🧪 Playwright
- 🤖 AI multi-provider
-
-
-
-
-
-
-
-
diff --git a/desktop/frontend-static/js/ai.js b/desktop/frontend-static/js/ai.js
deleted file mode 100644
index 2fe9d62..0000000
--- a/desktop/frontend-static/js/ai.js
+++ /dev/null
@@ -1,457 +0,0 @@
-/* ObsiGate — AI Editor Toolbar
- Provides AI-powered text editing features:
- - Inline completion (Ctrl+J)
- - Edit (improve, fix spelling, shorter, longer, simplify)
- - Tone (professional, casual)
- - Translate (multiple languages)
- - Generate (explain, summarize, continue)
- - AI Custom Rewrite
- - AI Toolbox (to list, to table, frontmatter, to canvas)
-*/
-import { api } from './auth.js';
-import { t } from './i18n.js';
-
-// ── API call helper ──
-async function aiAction(endpoint, text, extra = {}) {
- const body = { text, ...extra };
- const data = await api(`/api/ai/${endpoint}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- });
- return data.result;
-}
-
-// ── Get selected text from CodeMirror ──
-function getSelection(editorView) {
- if (!editorView) return '';
- const { from, to } = editorView.state.selection.main;
- if (from === to) {
- // No selection — get entire document
- return editorView.state.doc.toString();
- }
- return editorView.state.doc.sliceString(from, to);
-}
-
-// ── Replace or insert text ──
-function replaceSelection(editorView, newText, mode = 'replace') {
- if (!editorView) return;
- const { from, to } = editorView.state.selection.main;
- if (mode === 'append') {
- // Insert at end of selection
- editorView.dispatch({
- changes: { from: to, insert: '\n' + newText },
- selection: { anchor: to + 1 + newText.length },
- });
- } else if (mode === 'before') {
- editorView.dispatch({
- changes: { from: from, insert: newText + '\n' },
- });
- } else {
- // Replace selection
- editorView.dispatch({
- changes: { from, to, insert: newText },
- selection: { anchor: from + newText.length },
- });
- }
-}
-
-// ── Show toast notification ──
-function showToast(msg, type = 'info') {
- // Use global showToast if available, otherwise console
- if (typeof window._obsigateShowToast === 'function') {
- window._obsigateShowToast(msg, type);
- }
-}
-
-// ── Dropdown menu builder ──
-function createMenu(items, parentEl) {
- // Remove any existing menu
- const existing = document.querySelector('.ai-dropdown-menu');
- if (existing) existing.remove();
-
- const menu = document.createElement('div');
- menu.className = 'ai-dropdown-menu';
-
- // Position relative to parent, but attached to body to avoid
- // being clipped by overflow:hidden on the editor container.
- const rect = parentEl.getBoundingClientRect();
- const menuTop = rect.bottom + 4;
- const menuLeft = rect.left;
-
- Object.assign(menu.style, {
- position: 'fixed',
- top: menuTop + 'px',
- left: menuLeft + 'px',
- background: 'var(--bg-primary)',
- border: '1px solid var(--border-color)',
- borderRadius: '6px',
- padding: '4px 0',
- minWidth: '200px',
- maxHeight: '60vh',
- overflowY: 'auto',
- zIndex: '10000',
- boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
- });
-
- // If menu overflows bottom of viewport, flip above the button
- requestAnimationFrame(() => {
- const menuRect = menu.getBoundingClientRect();
- if (menuRect.bottom > window.innerHeight - 8) {
- menu.style.top = Math.max(8, rect.top - menuRect.height - 4) + 'px';
- }
- // If menu overflows right edge, align right
- if (menuRect.right > window.innerWidth - 8) {
- menu.style.left = Math.max(8, window.innerWidth - menuRect.width - 8) + 'px';
- }
- });
-
- 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',
- transition: 'background 0.1s',
- });
- 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);
- }
- if (item.children) {
- const arrow = document.createElement('span');
- arrow.style.cssText = 'margin-left:auto;font-size:0.7rem;color:var(--text-muted)';
- arrow.textContent = '▶';
- el.appendChild(arrow);
- }
- el.addEventListener('mouseenter', () => {
- el.style.background = 'rgba(255,255,255,0.06)';
- // Show submenu
- if (item.children) {
- const sub = el.querySelector('.ai-submenu');
- if (!sub) {
- const subMenu = createSubMenu(item.children, el);
- el.appendChild(subMenu);
- }
- }
- });
- el.addEventListener('mouseleave', (e) => {
- el.style.background = 'transparent';
- });
- el.addEventListener('click', (e) => {
- e.stopPropagation();
- if (item.action) item.action();
- menu.remove();
- });
- menu.appendChild(el);
- });
-
- document.body.appendChild(menu);
-
- // Close on outside click
- const closeHandler = (e) => {
- if (!menu.contains(e.target)) {
- menu.remove();
- document.removeEventListener('click', closeHandler);
- }
- };
- setTimeout(() => document.addEventListener('click', closeHandler), 0);
-}
-
-function createSubMenu(items, parentEl) {
- const menu = document.createElement('div');
- menu.className = 'ai-submenu';
- Object.assign(menu.style, {
- position: 'absolute',
- left: '100%',
- top: '0',
- background: 'var(--bg-primary)',
- border: '1px solid var(--border-color)',
- borderRadius: '6px',
- padding: '4px 0',
- minWidth: '180px',
- zIndex: '101',
- 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',
- fontSize: '0.78rem',
- color: 'var(--text-primary)',
- whiteSpace: 'nowrap',
- });
- el.innerHTML = item.label;
- 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();
- // Close all menus
- document.querySelectorAll('.ai-dropdown-menu, .ai-submenu').forEach(m => m.remove());
- });
- menu.appendChild(el);
- });
-
- return menu;
-}
-
-// ── Main AI Toolbar ──
-export async function createAIToolbar(container, getEditorView) {
- // Check if AI is configured
- let aiConfigured = false;
- try {
- const status = await api('/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 = t('ai.not_configured');
- container.appendChild(hint);
- return;
- }
-
- 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',
- });
-
- function ev() { return getEditorView(); }
-
- // ── Inline completion 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 = t('ai.inline_completion_title');
- aiBtn.addEventListener('click', async () => {
- const v = ev();
- if (!v) return;
- const text = getSelection(v);
- if (!text.trim()) return;
- const origHTML = aiBtn.innerHTML;
- aiBtn.innerHTML = '⏳ AI...';
- aiBtn.disabled = true;
- try {
- const result = await aiAction('inline-complete', text);
- replaceSelection(v, result, 'append');
- showToast(t('ai.completion_added'), 'success');
- } catch (e) {
- showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error');
- } finally {
- aiBtn.innerHTML = origHTML;
- aiBtn.disabled = false;
- }
- });
-
- // ── Edit menu ──
- const editBtn = createDropdownBtn(t('ai.edit'), [
- { label: '🪄 Improve writing', action: () => action('improve'), hint: t('ai.hint_improve') },
- { label: '🔤 Fix spelling & grammar', action: () => action('fix-spelling'), hint: 'Corrige les fautes' },
- { label: '📏 Make shorter', action: () => action('make-shorter'), hint: 'Rend le texte plus concis' },
- { label: '📐 Make longer', action: () => action('make-longer'), hint: t('ai.hint_add_details') },
- { label: '📋 Simplify language', action: () => action('simplify'), hint: 'Simplifie le langage' },
- ], t('ai.hint_edit_selection'));
-
- // ── Tone menu ──
- const toneBtn = createDropdownBtn('Ton', [
- { label: '💼 Professional tone', action: () => action('tone', { tone: 'professional' }) },
- { label: '💬 Casual tone', action: () => action('tone', { tone: 'casual' }) },
- ], 'Change le ton du texte');
-
- // ── Translate menu ──
- const translateBtn = createDropdownBtn('Traduire', [
- { label: '🇬🇧 English', action: () => action('translate', { target_lang: 'English' }) },
- { label: '🇨🇳 Chinese', action: () => action('translate', { target_lang: 'Chinese' }) },
- { label: '🇯🇵 Japanese', action: () => action('translate', { target_lang: 'Japanese' }) },
- { label: '🇩🇪 German', action: () => action('translate', { target_lang: 'German' }) },
- { label: '🇫🇷 French', action: () => action('translate', { target_lang: 'French' }) },
- { label: '🇪🇸 Spanish', action: () => action('translate', { target_lang: 'Spanish' }) },
- ], t('ai.hint_translate'));
-
- // ── Generate menu ──
- const genBtn = createDropdownBtn(t('ai.generate'), [
- { label: 'ℹ️ Explain this', action: () => action('explain', {}, 'append') },
- { label: '📝 Summarize', action: () => action('summarize', {}, 'append') },
- { label: '✏️ Continue writing', action: () => action('continue', {}, 'append') },
- ], t('ai.hint_generate'));
-
- // ── Custom Rewrite ──
- const rewriteBtn = document.createElement('button');
- rewriteBtn.className = 'ai-toolbar-btn';
- rewriteBtn.innerHTML = t('ai.rewrite');
- rewriteBtn.title = t('ai.rewrite_title');
- 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 = getSelection(v);
- const instruction = prompt(t('ai.rewrite_prompt'), '');
- if (!instruction) return;
- try {
- const result = await aiAction('rewrite', text, { instruction });
- replaceSelection(v, result);
- showToast(t('ai.text_rewritten'), 'success');
- } catch (e) {
- showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error');
- }
- });
-
- // ── Toolbox menu ──
- const toolboxBtn = createDropdownBtn(t('ai.toolbox'), [
- { label: '📋 Convert to list', action: () => action('to-list') },
- { label: '📊 Convert to table', action: () => action('to-table') },
- { label: '⚙️ Generate frontmatter', action: () => action('frontmatter', {}, 'before') },
- { label: '🔷 Convert to canvas', action: () => action('to-canvas', {}, 'append') },
- ], 'Outils de conversion');
-
- toolbar.appendChild(aiBtn);
- toolbar.appendChild(createSeparator());
- toolbar.appendChild(editBtn);
- toolbar.appendChild(toneBtn);
- toolbar.appendChild(translateBtn);
- toolbar.appendChild(createSeparator());
- toolbar.appendChild(genBtn);
- toolbar.appendChild(rewriteBtn);
- toolbar.appendChild(toolboxBtn);
-
- container.insertBefore(toolbar, container.firstChild);
-
- // ── Action helper ──
- async function action(endpoint, extra = {}, mode = 'replace') {
- const v = ev();
- if (!v) return;
- const text = getSelection(v);
- if (!text.trim()) {
- showToast(t('ai.select_text'), 'warning');
- return;
- }
- showToast(t('ai.processing'), 'info');
-
- // Show processing indicator in the editor header
- _showProcessingIndicator(true);
-
- // Dispatch AI processing start event
- document.dispatchEvent(new CustomEvent('ai-processing-start', { detail: { endpoint, mode } }));
- try {
- const result = await aiAction(endpoint, text, extra);
- replaceSelection(v, result, mode);
- showToast(t('ai.text_processed'), 'success');
- } catch (e) {
- const msg = String(e.message || e);
- if (msg.includes('401')) {
- showToast(t('ai.invalid_api_key'), 'error');
- } else if (msg.includes('402') || msg.includes('429')) {
- showToast(t('ai.quota_exceeded'), 'error');
- } else {
- showToast('AI: ' + msg, 'error');
- }
- } finally {
- _showProcessingIndicator(false);
- // Dispatch AI processing end event
- document.dispatchEvent(new CustomEvent('ai-processing-end'));
- }
- }
-
- // ── In-editor processing indicator ──
- function _showProcessingIndicator(show) {
- let el = document.getElementById('ai-processing-indicator');
- if (show) {
- if (!el) {
- el = document.createElement('span');
- el.id = 'ai-processing-indicator';
- el.className = 'ai-processing-indicator';
- el.innerHTML = ' AI...';
- el.title = 'Traitement AI en cours';
- const header = document.querySelector('#editor-modal .editor-header');
- if (header) header.appendChild(el);
- }
- el.style.display = 'inline-flex';
- } else if (el) {
- el.style.display = 'none';
- }
- }
- // ── Keyboard shortcut Ctrl+J for inline completion ──
- document.addEventListener('keydown', (e) => {
- if (e.ctrlKey && e.key === 'j') {
- const modal = document.getElementById('editor-modal');
- if (!modal || !modal.classList.contains('active')) return;
- e.preventDefault();
- aiBtn.click();
- }
- });
-
- return toolbar;
-}
-
-function createDropdownBtn(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();
- createMenu(items, btn);
- });
- btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.08)'; });
- btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
- return btn;
-}
-
-function createSeparator() {
- const sep = document.createElement('span');
- sep.style.cssText = 'width:1px;height:16px;background:var(--border-color);margin:0 2px';
- return sep;
-}
diff --git a/desktop/frontend-static/js/app.js b/desktop/frontend-static/js/app.js
deleted file mode 100644
index c878525..0000000
--- a/desktop/frontend-static/js/app.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/* ObsiGate — Module orchestrator (ES modules).
- Replaces the monolithic frontend/app.js IIFE.
- All code lives in frontend/js/*.js modules. */
-
-import * as UI from './ui.js';
-import * as Utils from './utils.js';
-import { initI18n, t } from './i18n.js';
-
-// Wire up AI toolbar toast (avoids circular import in utils.js)
-window._obsigateShowToast = UI.showToast;
-import * as Sidebar from './sidebar.js';
-import * as Auth from './auth.js';
-import * as Viewer from './viewer.js';
-import * as Sync from './sync.js';
-import { initGraphView } from './graph.js';
-import * as Legacy from './legacy.js';
-import { initCommandPalette } from './palette.js';
-import { initMobileToolbar } from './mobile-toolbar.js';
-import { initDragDrop } from './dragdrop.js';
-import { initMermaid, setupFocusMode } from './mermaid-viewer.js';
-import PaneManager from './pane-manager.js';
-
-// =========================================================================
-// Initialization — mirrors the original app.js init() ordering
-// =========================================================================
-async function init() {
- // 1. Init i18n FIRST — loads fallback (FR) + selected locale
- // Server language check may fail (no auth yet), falls to localStorage
- // LocalStorage has correct language if user set it previously
- await initI18n();
-
- // 2. Init auth — now t() works inside showApp()/renderUserMenu()
- const authOk = await Auth.AuthManager.initAuth();
-
- UI.initTheme();
- UI.initHeaderMenu();
- UI.initCustomDropdowns();
-
- document.getElementById("theme-toggle").addEventListener("click", UI.toggleTheme);
- document.getElementById("header-logo").addEventListener("click", Legacy.goHome);
- const refreshBtn = document.getElementById("header-refresh-btn");
- if (refreshBtn) refreshBtn.addEventListener("click", Legacy.goHome);
-
- Legacy.initSearch();
- UI.initSidebarToggle();
- UI.initMobile();
- Sidebar.initVaultContext();
- Legacy.initSidebarTabs();
- Legacy.initHelpModal();
- Legacy.initConfigModal();
- Legacy.initAboutModal();
- Sidebar.initSidebarFilter();
- UI.initSidebarResize();
- Utils.initEditor();
- Auth.initLoginForm();
- Legacy.initRecentTab();
-
- UI.RightSidebarManager.init();
- UI.FindInPageManager.init();
- UI.ContextMenuManager.init();
- UI.TabManager.init();
- PaneManager.init();
- initCommandPalette();
- initMobileToolbar();
-
- if (authOk) {
- Legacy.initSyncStatus();
-
- try {
- await Promise.all([Legacy.loadVaultSettings(), Sidebar.loadVaults(), Sidebar.loadTags()]);
-
- initDragDrop();
-
- Legacy.DashboardRecentWidget.init();
-
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get("popup") === "true") {
- document.body.classList.add("popup-mode");
- }
-
- if (window.location.hash && window.location.hash.startsWith("#file=")) {
- const hashVal = window.location.hash.substring(6);
- const sepIndex = hashVal.indexOf(":");
- if (sepIndex > -1) {
- const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
- const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
- Viewer.openFile(vault, path);
- }
- } else if (urlParams.get("popup") !== "true") {
- Legacy.showWelcome();
- }
- } catch (err) {
- console.error("Failed to initialize ObsiGate:", err);
- UI.showToast(t("error.initialization"), "error");
- }
- }
-
- initGraphView();
- initMermaid();
- setupFocusMode();
- Utils.safeCreateIcons();
-}
-
-document.addEventListener("DOMContentLoaded", () => {
- init();
- Sync.init();
-});
diff --git a/desktop/frontend-static/js/auth.js b/desktop/frontend-static/js/auth.js
deleted file mode 100644
index 6c2d8f4..0000000
--- a/desktop/frontend-static/js/auth.js
+++ /dev/null
@@ -1,579 +0,0 @@
-/* ObsiGate — Authentication: API helper, AuthManager, login form, AdminPanel */
-import { state } from './state.js';
-import { safeCreateIcons } from './utils.js';
-import { showToast, closeHeaderMenu } from './ui.js';
-import { t, getLocale, setLocale } from './i18n.js';
-import { showWelcome } from './viewer.js';
-
-// ── Global logout handler for header dropdown onclick ────────────────────
-// Defined at module top level so it's always available, even if
-// renderUserMenu() hasn't been called yet.
-// Uses a getter to lazy-resolve AuthManager (defined later in this module).
-window.handleLogout = () => {
- AuthManager.logout();
-};
-
-// ---------------------------------------------------------------------------
-// API helpers
-// ---------------------------------------------------------------------------
-
-/**
- * Fetch JSON from an API endpoint with optional AbortSignal support.
- * Surfaces errors to the user via toast instead of silently failing.
- *
- * @param {string} path - API URL path.
- * @param {object} [opts] - Fetch options (may include signal).
- * @returns {Promise} Parsed JSON response.
- */
-async function api(path, opts) {
- var res;
- try {
- // Inject auth header if authenticated
- const authHeaders = AuthManager.getAuthHeaders();
- const mergedOpts = opts || {};
- // Auto-set Content-Type for JSON bodies
- if (mergedOpts.body && typeof mergedOpts.body === "string" && !mergedOpts.headers?.["Content-Type"]) {
- mergedOpts.headers = { ...mergedOpts.headers, "Content-Type": "application/json" };
- }
- if (authHeaders) {
- mergedOpts.headers = { ...mergedOpts.headers, ...authHeaders };
- }
- mergedOpts.credentials = "include";
- res = await fetch(path, mergedOpts);
- } catch (err) {
- if (err.name === "AbortError") throw err; // let callers handle abort
- showToast(t("auth.network_error"), "error");
- throw err;
- }
- if (res.status === 401 && AuthManager._authEnabled) {
- // Token expired — try refresh
- try {
- await AuthManager.refreshAccessToken();
- // Retry the request with new token
- const retryHeaders = AuthManager.getAuthHeaders();
- const retryOpts = opts || {};
- retryOpts.headers = { ...retryOpts.headers, ...retryHeaders };
- retryOpts.credentials = "include";
- res = await fetch(path, retryOpts);
- } catch (refreshErr) {
- AuthManager.clearSession();
- AuthManager.showLoginScreen();
- throw new Error("Session expirée");
- }
- }
- if (!res.ok) {
- var detail = "";
- try {
- var body = await res.json();
- detail = body.detail || "";
- } catch (_) {
- /* no json body */
- }
- showToast(detail || "Erreur API : " + res.status, "error");
- throw new Error(detail || "API error: " + res.status);
- }
- return res.json();
-}
-
-
-// ---------------------------------------------------------------------------
-// AuthManager — Authentication state & token management
-// ---------------------------------------------------------------------------
-
-const AuthManager = {
- ACCESS_TOKEN_KEY: "obsigate_access_token",
- TOKEN_EXPIRY_KEY: "obsigate_token_expiry",
- USER_KEY: "obsigate_user",
- _authEnabled: false,
-
- // ── Token storage (sessionStorage) ─────────────────────────────
-
- saveToken(tokenData) {
- const expiresAt = Date.now() + tokenData.expires_in * 1000;
- sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token);
- sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString());
- if (tokenData.user) {
- sessionStorage.setItem(this.USER_KEY, JSON.stringify(tokenData.user));
- }
- },
-
- getToken() {
- return sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
- },
-
- getUser() {
- const raw = sessionStorage.getItem(this.USER_KEY);
- return raw ? JSON.parse(raw) : null;
- },
-
- isTokenExpired() {
- const expiry = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY);
- if (!expiry) return true;
- // Renew 60s before expiration
- return Date.now() > parseInt(expiry) - 60000;
- },
-
- clearSession() {
- sessionStorage.removeItem(this.ACCESS_TOKEN_KEY);
- sessionStorage.removeItem(this.TOKEN_EXPIRY_KEY);
- sessionStorage.removeItem(this.USER_KEY);
- },
-
- getAuthHeaders() {
- const token = this.getToken();
- if (!token || !this._authEnabled) return null;
- return { Authorization: "Bearer " + token };
- },
-
- // ── API calls ──────────────────────────────────────────────────
-
- async login(username, password, rememberMe) {
- const response = await fetch("/api/auth/login", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
- body: JSON.stringify({ username, password, remember_me: rememberMe || false }),
- });
- if (!response.ok) {
- const err = await response.json();
- throw new Error(err.detail || "Erreur de connexion");
- }
- const data = await response.json();
- this.saveToken(data);
- return data.user;
- },
-
- async logout() {
- try {
- const token = this.getToken();
- await fetch("/api/auth/logout", {
- method: "POST",
- headers: token ? { Authorization: "Bearer " + token } : {},
- credentials: "include",
- });
- } catch (e) {
- /* continue even if API fails */
- }
- this.clearSession();
- this.showLoginScreen();
- },
-
- async refreshAccessToken() {
- const response = await fetch("/api/auth/refresh", {
- method: "POST",
- credentials: "include",
- });
- if (!response.ok) {
- this.clearSession();
- throw new Error("Session expirée");
- }
- const data = await response.json();
- const expiry = Date.now() + data.expires_in * 1000;
- sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token);
- sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString());
- return data.access_token;
- },
-
- // ── UI controls ────────────────────────────────────────────────
-
- showLoginScreen() {
- const app = document.getElementById("app");
- const login = document.getElementById("login-screen");
- if (app) app.classList.add("hidden");
- if (login) {
- login.classList.remove("hidden");
- const usernameInput = document.getElementById("login-username");
- if (usernameInput) usernameInput.focus();
- }
- },
-
- showApp() {
- const login = document.getElementById("login-screen");
- const app = document.getElementById("app");
- if (login) login.classList.add("hidden");
- if (app) app.classList.remove("hidden");
- this.renderUserMenu();
- },
-
- renderUserMenu() {
- const user = this.getUser();
- const userMenu = document.getElementById("user-menu");
- if (!userMenu) return;
- if (!user || !this._authEnabled) {
- userMenu.innerHTML = "";
- return;
- }
- userMenu.innerHTML = '' + (user.display_name || user.username) + " " + ' ';
- safeCreateIcons();
-
- const logoutBtn = document.getElementById("logout-btn");
- if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout());
-
- const adminRow = document.getElementById("admin-menu-row");
- if (adminRow) {
- if (user.role === "admin") {
- adminRow.classList.remove("hidden");
- // Important: use an inline function to ensure we don't bind multiple identical listeners on rerenders, or clean up before
- adminRow.onclick = () => {
- closeHeaderMenu();
- AdminPanel.show();
- };
- } else {
- adminRow.classList.add("hidden");
- }
- }
- },
-
- // ── Initialization ──────────────────────────────────────────────
-
- async checkAuthStatus() {
- try {
- const res = await fetch("/api/auth/status");
- const data = await res.json();
- this._authEnabled = data.auth_enabled;
- return data;
- } catch (e) {
- this._authEnabled = false;
- return { auth_enabled: false };
- }
- },
-
- async initAuth() {
- const status = await this.checkAuthStatus();
- if (!status.auth_enabled) {
- // Auth disabled — show app immediately
- this.showApp();
- return true;
- }
-
- // Auth enabled — check for existing session
- if (this.getToken() && !this.isTokenExpired()) {
- this.showApp();
- return true;
- }
-
- // Try silent refresh
- try {
- await this.refreshAccessToken();
- // Fetch user info
- const token = this.getToken();
- const res = await fetch("/api/auth/me", {
- headers: { Authorization: "Bearer " + token },
- credentials: "include",
- });
- if (res.ok) {
- const user = await res.json();
- sessionStorage.setItem(this.USER_KEY, JSON.stringify(user));
- this.showApp();
- return true;
- }
- } catch (e) {
- /* silent refresh failed */
- }
-
- // No valid session — show login
- this.showLoginScreen();
- return false;
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Login form handler
-// ---------------------------------------------------------------------------
-
-function initLoginForm() {
- const form = document.getElementById("login-form");
- if (!form) return;
-
- form.addEventListener("submit", async (e) => {
- e.preventDefault();
- const username = document.getElementById("login-username").value;
- const password = document.getElementById("login-password").value;
- const rememberMe = document.getElementById("remember-me").checked;
- const errorEl = document.getElementById("login-error");
- const btn = document.getElementById("login-btn");
-
- btn.disabled = true;
- btn.querySelector(".btn-spinner").classList.remove("hidden");
- btn.querySelector(".btn-text").textContent = "Connexion...";
- errorEl.classList.add("hidden");
-
- try {
- await AuthManager.login(username, password, rememberMe);
- AuthManager.showApp();
-
- // Re-sync language from server now that we're authenticated
- try {
- const resp = await fetch('/api/auth/me', { credentials: 'include' });
- if (resp.ok) {
- const user = await resp.json();
- if (user.language && user.language !== getLocale()) {
- await setLocale(user.language);
- }
- }
- } catch (e) { /* non-bloquant */ }
-
- // Load app data after successful login
- try {
- // Dynamic imports to avoid circular dependency with sidebar.js
- const { loadVaults, loadTags } = await import('./sidebar.js');
- await Promise.all([loadVaults(), loadTags()]);
- // Start SSE sync now that auth cookie is set (dynamic import to avoid circular dep)
- const { IndexUpdateManager } = await import('./sync.js');
- IndexUpdateManager.connect();
- // Sync offline file index now that we're authenticated
- const { syncFileIndexFromServer } = await import('./offline.js');
- syncFileIndexFromServer();
- // Show dashboard
- showWelcome();
- } catch (err) {
- console.error("Failed to load data after login:", err);
- }
- safeCreateIcons();
- } catch (err) {
- errorEl.textContent = err.message;
- errorEl.classList.remove("hidden");
- document.getElementById("login-password").value = "";
- document.getElementById("login-password").focus();
- } finally {
- btn.disabled = false;
- btn.querySelector(".btn-spinner").classList.add("hidden");
- btn.querySelector(".btn-text").textContent = "Se connecter";
- }
- });
-
- // Toggle password visibility
- const toggleBtn = document.getElementById("toggle-password");
- if (toggleBtn) {
- toggleBtn.addEventListener("click", () => {
- const input = document.getElementById("login-password");
- input.type = input.type === "password" ? "text" : "password";
- });
- }
-}
-
-
-// ---------------------------------------------------------------------------
-// Admin Panel — User management (admin only)
-// ---------------------------------------------------------------------------
-
-const AdminPanel = {
- _modal: null,
- _allVaults: [],
-
- show() {
- this._createModal();
- this._modal.classList.add("active");
- this._loadUsers();
- },
-
- hide() {
- if (this._modal) this._modal.classList.remove("active");
- },
-
- _createModal() {
- if (this._modal) return;
- this._modal = document.createElement("div");
- this._modal.className = "editor-modal";
- this._modal.id = "admin-modal";
- this._modal.innerHTML = `
-
-
-
-
- ${t('admin.new_user')}
-
-
-
-
- `;
- document.body.appendChild(this._modal);
- safeCreateIcons();
-
- document.getElementById("admin-close").addEventListener("click", () => this.hide());
- document.getElementById("admin-add-user").addEventListener("click", () => this._showUserForm(null));
- },
-
- async _loadUsers() {
- try {
- const users = await api("/api/auth/admin/users");
- // Also load available vaults
- try {
- const vaultsData = await api("/api/vaults");
- this._allVaults = vaultsData.map((v) => v.name);
- } catch (e) {
- this._allVaults = [];
- }
- this._renderUsers(users);
- } catch (err) {
- document.getElementById("admin-users-list").innerHTML = 'Erreur : ' + err.message + "
";
- }
- },
-
- _renderUsers(users) {
- const container = document.getElementById("admin-users-list");
- if (!users.length) {
- container.innerHTML = 'Aucun utilisateur.
';
- return;
- }
- let html = '' +
- '' + t('admin.col_user') + ' ' + t('admin.col_role') + ' ' + t('admin.col_vaults') + ' ' + t('admin.col_status') + ' ' + t('admin.col_last_login') + ' ' + t('admin.col_actions') + ' ' +
- ' ';
- users.forEach((u) => {
- const vaults = u.vaults.includes("*") ? t('common.all') : u.vaults.join(", ") || t('common.none');
- const status = u.active ? "✅" : "🔴";
- const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US', { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : t('common.never');
- html +=
- "" +
- "" +
- u.username +
- " " +
- (u.display_name && u.display_name !== u.username ? "" + u.display_name + " " : "") +
- " " +
- '' +
- u.role +
- " " +
- '' +
- vaults +
- " " +
- "" +
- status +
- " " +
- "" +
- lastLogin +
- " " +
- '' +
- '✏️ ' +
- '🗑️ ' +
- " ";
- });
- html += "
";
- container.innerHTML = html;
-
- // Bind action buttons
- container.querySelectorAll('[data-action="edit"]').forEach((btn) => {
- btn.addEventListener("click", () => {
- const user = users.find((u) => u.username === btn.dataset.username);
- if (user) this._showUserForm(user);
- });
- });
- container.querySelectorAll('[data-action="delete"]').forEach((btn) => {
- btn.addEventListener("click", () => this._deleteUser(btn.dataset.username));
- });
- },
-
- _showUserForm(user) {
- const isEdit = !!user;
- const title = isEdit ? "Modifier : " + user.username : "Nouvel utilisateur";
- const vaultCheckboxes = this._allVaults
- .map((v) => {
- const checked = user && (user.vaults.includes(v) || user.vaults.includes("*")) ? "checked" : "";
- return '" + v + " ";
- })
- .join("");
- const allVaultsChecked = user && user.vaults.includes("*") ? "checked" : "";
-
- // Create form modal overlay
- const overlay = document.createElement("div");
- overlay.className = "admin-form-overlay";
- overlay.innerHTML = `
-
- `;
- this._modal.appendChild(overlay);
-
- document.getElementById("admin-form-cancel").addEventListener("click", () => overlay.remove());
-
- document.getElementById("admin-user-form").addEventListener("submit", async (e) => {
- e.preventDefault();
- const form = e.target;
- const allVaults = document.getElementById("admin-all-vaults").checked;
- const selectedVaults = allVaults ? ["*"] : Array.from(form.querySelectorAll('input[name="vault"]:checked')).map((cb) => cb.value);
-
- try {
- if (isEdit) {
- const updates = {
- display_name: form.display_name.value || null,
- role: form.role.value,
- vaults: selectedVaults,
- };
- if (form.password.value) updates.password = form.password.value;
- const activeCheckbox = form.querySelector('input[name="active"]');
- if (activeCheckbox) updates.active = activeCheckbox.checked;
- await api("/api/auth/admin/users/" + user.username, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(updates),
- });
- } else {
- await api("/api/auth/admin/users", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- username: form.username.value,
- password: form.password.value,
- display_name: form.display_name.value || null,
- role: form.role.value,
- vaults: selectedVaults,
- }),
- });
- }
- overlay.remove();
- this._loadUsers();
- showToast(isEdit ? "Utilisateur modifié" : "Utilisateur créé", "success");
- } catch (err) {
- showToast(err.message, "error");
- }
- });
- },
-
- async _deleteUser(username) {
- const currentUser = AuthManager.getUser();
- if (currentUser && currentUser.username === username) {
- showToast(t("auth.cannot_delete_self"), "error");
- return;
- }
- if (!confirm("Supprimer l'utilisateur \"" + username + '" ?')) return;
- try {
- await api("/api/auth/admin/users/" + username, { method: "DELETE" });
- this._loadUsers();
- showToast(t("auth.user_deleted"), "success");
- } catch (err) {
- showToast(err.message, "error");
- }
- },
-};
-
-
-export { api, AuthManager, initLoginForm, AdminPanel };
diff --git a/desktop/frontend-static/js/autocomplete.js b/desktop/frontend-static/js/autocomplete.js
deleted file mode 100644
index 86492aa..0000000
--- a/desktop/frontend-static/js/autocomplete.js
+++ /dev/null
@@ -1,350 +0,0 @@
-/* ObsiGate — Autocomplete intelligent
- * Partagé entre éditeur Forge (textarea) et CodeMirror.
- * Détecte le contexte autour du curseur et suggère des complétions.
- *
- * Contexte supporté : mermaid, code fence, frontmatter, markdown, wikilink, table, texte libre.
- *
- * API :
- * suggest(text, cursorPos) → { context, items: [{label, detail, insert, from, to}] }
- * CompletionSource pour CodeMirror : codeMirrorCompletionSource(context)
- */
-
-// ── Suggestion Data ──────────────────────────────────────────────────
-var MERMAID_TYPES = [
- { label: 'flowchart TD', detail: 'Diagramme de flux (top-down)', insert: 'flowchart TD\n ' },
- { label: 'flowchart LR', detail: 'Diagramme de flux (gauche-droite)', insert: 'flowchart LR\n ' },
- { label: 'sequenceDiagram', detail: 'Sequence (acteurs + messages)', insert: 'sequenceDiagram\n ' },
- { label: 'classDiagram', detail: 'Diagramme de classes UML', insert: 'classDiagram\n ' },
- { label: 'stateDiagram-v2', detail: 'Diagramme d\'etats', insert: 'stateDiagram-v2\n ' },
- { label: 'erDiagram', detail: 'Diagramme Entite-Relation', insert: 'erDiagram\n ' },
- { label: 'journey', detail: 'Parcours utilisateur', insert: 'journey\n ' },
- { label: 'gantt', detail: 'Diagramme de Gantt', insert: 'gantt\n ' },
- { label: 'pie', detail: 'Diagramme circulaire', insert: 'pie\n ' },
- { label: 'gitGraph', detail: 'Historique Git', insert: 'gitGraph\n ' },
- { label: 'mindmap', detail: 'Carte mentale', insert: 'mindmap\n ' },
- { label: 'timeline', detail: 'Ligne du temps', insert: 'timeline\n ' },
- { label: 'quadrantChart', detail: 'Graphique quadrants', insert: 'quadrantChart\n ' },
- { label: 'xychart-beta', detail: 'Graphique XY', insert: 'xychart-beta\n ' },
- { label: 'block-beta', detail: 'Diagramme de blocs', insert: 'block-beta\n ' }
-];
-
-var MERMAID_SYNTAX = {
- 'flowchart': [
- { label: 'A-->B', detail: 'Fleche', insert: '-->' },
- { label: 'A--text-->B', detail: 'Fleche avec texte', insert: '-- texte -->' },
- { label: 'A-.->B', detail: 'Fleche pointillee', insert: '-.->' },
- { label: 'A==>B', detail: 'Fleche epaisse', insert: '==>' },
- { label: 'subgraph', detail: 'Sous-graphe', insert: 'subgraph titre\n \nend' },
- { label: 'style A fill:#xxx', detail: 'Style de noeud', insert: 'style A fill:#333,stroke:#58a6ff' },
- { label: 'classDef', detail: 'Classe CSS', insert: 'classDef default fill:#333,stroke:#58a6ff' }
- ],
- 'sequenceDiagram': [
- { label: 'participant X', detail: 'Declarer acteur', insert: 'participant Nom' },
- { label: 'A->>B: msg', detail: 'Message', insert: '->> : ' },
- { label: 'A-->>B: msg', detail: 'Message asynchrone', insert: '-->> : ' },
- { label: 'Note over A,B', detail: 'Note', insert: 'Note over A,B: ' },
- { label: 'loop text', detail: 'Boucle', insert: 'loop description\n \nend' },
- { label: 'alt/else', detail: 'Alternative', insert: 'alt condition\n \nelse\n \nend' },
- { label: 'activate A', detail: 'Activer ligne de vie', insert: 'activate A' }
- ],
- 'classDiagram': [
- { label: 'class Nom {', detail: 'Declarer classe', insert: 'class Nom {\n \n}' },
- { label: '+methode()', detail: 'Methode publique', insert: '+nom() Type' },
- { label: '-attribut', detail: 'Attribut prive', insert: '-nom Type' },
- { label: 'Heritage', detail: 'Relation heritage', insert: ' <|-- ' },
- { label: 'Composition', detail: 'Relation composition', insert: ' *-- ' }
- ],
- 'gantt': [
- { label: 'dateFormat', detail: 'Format de date', insert: 'dateFormat YYYY-MM-DD' },
- { label: 'section', detail: 'Section Gantt', insert: 'section Nom' },
- { label: 'tache : apres', detail: 'Tache avec dependance', insert: ' : after tache1, 3d' }
- ],
- 'pie': [
- { label: 'title', detail: 'Titre du pie', insert: 'title Titre du graphique' },
- { label: '"Label" : val', detail: 'Valeur', insert: '"Label" : 50' }
- ],
- 'mindmap': [
- { label: 'root', detail: 'Racine', insert: 'root((Sujet))' },
- { label: 'branche', detail: 'Sous-sujet', insert: '\n Sous-sujet' },
- { label: 'leaf', detail: 'Feuille', insert: '\n Detail' }
- ]
-};
-
-var CODE_LANGUAGES = [
- { label: 'python', detail: 'Python', insert: 'python' },
- { label: 'javascript', detail: 'JavaScript', insert: 'javascript' },
- { label: 'typescript', detail: 'TypeScript', insert: 'typescript' },
- { label: 'bash', detail: 'Bash / Shell', insert: 'bash' },
- { label: 'sql', detail: 'SQL', insert: 'sql' },
- { label: 'json', detail: 'JSON', insert: 'json' },
- { label: 'yaml', detail: 'YAML', insert: 'yaml' },
- { label: 'html', detail: 'HTML', insert: 'html' },
- { label: 'css', detail: 'CSS', insert: 'css' },
- { label: 'dockerfile', detail: 'Dockerfile', insert: 'dockerfile' },
- { label: 'mermaid', detail: 'Mermaid (diagrammes)', insert: 'mermaid' },
- { label: 'markdown', detail: 'Markdown', insert: 'markdown' },
- { label: 'rust', detail: 'Rust', insert: 'rust' },
- { label: 'go', detail: 'Go', insert: 'go' },
- { label: 'java', detail: 'Java', insert: 'java' }
-];
-
-var CODE_SNIPPETS = {
- python: [
- { label: 'def', detail: 'Fonction', insert: 'def nom(params):\n ' },
- { label: 'class', detail: 'Classe', insert: 'class Nom:\n def __init__(self):\n ' },
- { label: 'if/else', detail: 'Condition', insert: 'if condition:\n \nelse:\n ' },
- { label: 'for', detail: 'Boucle for', insert: 'for item in iterable:\n ' },
- { label: 'try/except', detail: 'Try-except', insert: 'try:\n \nexcept Exception as e:\n ' },
- { label: 'import', detail: 'Import', insert: 'import ' },
- { label: 'print', detail: 'Print', insert: 'print()' },
- { label: 'lambda', detail: 'Lambda', insert: 'lambda x: ' }
- ],
- javascript: [
- { label: 'function', detail: 'Fonction', insert: 'function nom(params) {\n \n}' },
- { label: 'const', detail: 'Constante', insert: 'const nom = ' },
- { label: '=>', detail: 'Fleche', insert: '(params) => {\n \n}' },
- { label: 'if/else', detail: 'Condition', insert: 'if (cond) {\n \n} else {\n \n}' },
- { label: 'for', detail: 'Boucle for', insert: 'for (let i = 0; i < n; i++) {\n \n}' },
- { label: 'try/catch', detail: 'Try-catch', insert: 'try {\n \n} catch (e) {\n \n}' },
- { label: 'console.log', detail: 'Console log', insert: 'console.log()' },
- { label: 'async/await', detail: 'Async function', insert: 'async function nom() {\n await \n}' }
- ],
- bash: [
- { label: '#!/bin/bash', detail: 'Shebang', insert: '#!/bin/bash' },
- { label: 'if/fi', detail: 'Condition', insert: 'if [ condition ]; then\n \nfi' },
- { label: 'for', detail: 'Boucle for', insert: 'for item in list; do\n \ndone' },
- { label: 'case/esac', detail: 'Case', insert: 'case var in\n pattern)\n ;;\nesac' },
- { label: 'function', detail: 'Fonction', insert: 'nom() {\n \n}' }
- ],
- sql: [
- { label: 'SELECT', detail: 'Select', insert: 'SELECT colonnes\nFROM table' },
- { label: 'JOIN', detail: 'Join', insert: 'JOIN table2 ON table1.id = table2.id' },
- { label: 'WHERE', detail: 'Where', insert: 'WHERE condition' },
- { label: 'GROUP BY', detail: 'Group by', insert: 'GROUP BY colonne' },
- { label: 'ORDER BY', detail: 'Order by', insert: 'ORDER BY colonne DESC' },
- { label: 'INSERT INTO', detail: 'Insert', insert: 'INSERT INTO table (cols) VALUES (vals)' },
- { label: 'CREATE TABLE', detail: 'Create table', insert: 'CREATE TABLE nom (\n id INTEGER PRIMARY KEY,\n \n)' }
- ]
-};
-
-var FRONTMATTER_FIELDS = [
- { label: 'title:', detail: 'Titre du document', insert: 'title: ' },
- { label: 'tags:', detail: 'Tags (liste)', insert: 'tags:\n - ' },
- { label: 'date:', detail: 'Date', insert: 'date: ' + new Date().toISOString().slice(0,10) },
- { label: 'status:', detail: 'Statut', insert: 'status: en cours' },
- { label: 'category:', detail: 'Categorie', insert: 'category: ' },
- { label: 'publish:', detail: 'Publier (true/false)', insert: 'publish: true' },
- { label: 'favoris:', detail: 'Favoris (true/false)', insert: 'favoris: true' },
- { label: 'author:', detail: 'Auteur', insert: 'author: ' },
- { label: 'description:', detail: 'Description', insert: 'description: ' }
-];
-
-var MARKDOWN_FORMAT = [
- { label: '# H1', detail: 'Titre niveau 1', insert: '# ' },
- { label: '## H2', detail: 'Titre niveau 2', insert: '## ' },
- { label: '### H3', detail: 'Titre niveau 3', insert: '### ' },
- { label: '**gras**', detail: 'Texte en gras', insert: '****' },
- { label: '*italique*', detail: 'Texte en italique', insert: '**' },
- { label: '`code`', detail: 'Code inline', insert: '``' },
- { label: '[lien](url)', detail: 'Lien', insert: '[]()' },
- { label: '', detail: 'Image', insert: '![]()' },
- { label: '- liste', detail: 'Liste a puces', insert: '- ' },
- { label: '1. liste', detail: 'Liste numerotee', insert: '1. ' },
- { label: '> citation', detail: 'Citation', insert: '> ' },
- { label: '---', detail: 'Separateur horizontal', insert: '\n---\n' },
- { label: '> [!note]', detail: 'Callout Note', insert: '> [!note] Titre\n> Contenu' },
- { label: '> [!warning]', detail: 'Callout Warning', insert: '> [!warning] Titre\n> Contenu' },
- { label: '> [!tip]', detail: 'Callout Tip', insert: '> [!tip] Titre\n> Contenu' },
- { label: '> [!danger]', detail: 'Callout Danger', insert: '> [!danger] Titre\n> Contenu' }
-];
-
-// ── Context Detection ─────────────────────────────────────────────────
-function detectContext(text, cursorPos) {
- var before = text.slice(0, cursorPos);
- var after = text.slice(cursorPos);
- var lines = before.split('\n');
- var currentLine = lines[lines.length - 1] || '';
- var lineNum = lines.length;
-
- // Check for code fence context (look backwards for unclosed ```)
- var inFence = null;
- var fenceLang = '';
- var fenceStart = 0;
- for (var i = lines.length - 1; i >= 0; i--) {
- var l = lines[i].trim();
- var m = l.match(/^```(\w*)/);
- if (m) {
- if (inFence === null) {
- inFence = true;
- fenceLang = m[1] || '';
- fenceStart = i + 1;
- } else {
- inFence = false;
- break;
- }
- }
- }
-
- if (inFence) {
- if (fenceLang === 'mermaid') return { type: 'mermaid', lang: '', line: currentLine };
- return { type: 'code', lang: fenceLang, line: currentLine };
- }
-
- // Check for frontmatter context (between first and second ---)
- if (lineNum >= 2) {
- var firstSep = -1, secondSep = -1;
- for (var j = 0; j < lines.length; j++) {
- if (lines[j].trim() === '---') {
- if (firstSep === -1) firstSep = j;
- else if (secondSep === -1) { secondSep = j; break; }
- }
- }
- if (firstSep === 0 && secondSep === -1) {
- return { type: 'frontmatter', line: currentLine };
- }
- }
-
- // Check for wikilink
- if (/\[\[$/.test(currentLine) || /\[\[[^\]]*$/.test(currentLine)) {
- var partial = (currentLine.match(/\[\[([^\]]*)$/) || ['',''])[1];
- return { type: 'wikilink', partial: partial, line: currentLine };
- }
-
- // Check for markdown link
- if (/\[.*\]\($/.test(currentLine)) {
- return { type: 'link', line: currentLine };
- }
-
- // Check for triple backtick (about to open fence)
- if (/```$/.test(before) || /^```\s*$/.test(currentLine)) {
- return { type: 'fence_open', line: currentLine };
- }
-
- // Check if inside a markdown table
- if (/^\|.*\|$/.test(currentLine.trim())) {
- return { type: 'table', line: currentLine };
- }
-
- // Plain text / markdown
- return { type: 'markdown', line: currentLine };
-}
-
-// ── Suggestion Engine ─────────────────────────────────────────────────
-function suggest(text, cursorPos) {
- var ctx = detectContext(text, cursorPos);
- var items = [];
- var prefix = '';
-
- var currentLine = ctx.line;
-
- switch (ctx.type) {
- case 'mermaid':
- // Try to match mermaid type already declared
- var mermaidMatch = text.match(/```mermaid\s*\n(\w+)/);
- if (mermaidMatch) {
- var mmType = mermaidMatch[1];
- items = MERMAID_SYNTAX[mmType] || [];
- }
- if (items.length === 0) {
- items = MERMAID_TYPES;
- }
- break;
-
- case 'code':
- items = CODE_SNIPPETS[ctx.lang] || [];
- break;
-
- case 'fence_open':
- items = CODE_LANGUAGES;
- break;
-
- case 'frontmatter':
- prefix = currentLine.trim();
- items = FRONTMATTER_FIELDS.filter(function(f) {
- return f.label.startsWith(prefix) || prefix === '';
- }).map(function(f) {
- return { label: f.label, detail: f.detail, insert: prefix ? f.insert : f.insert };
- });
- return {
- context: ctx.type,
- items: items.length > 0 ? items : FRONTMATTER_FIELDS.map(function(f) {
- return { label: f.label, detail: f.detail, insert: f.insert };
- })
- };
-
- case 'wikilink':
- return {
- context: 'wikilink',
- items: [],
- partial: ctx.partial,
- async: true // requires file index lookup
- };
-
- case 'markdown':
- default:
- // At start of line: markdown formatting
- if (currentLine.trim().length < 3) {
- items = MARKDOWN_FORMAT;
- } else {
- items = [];
- }
- break;
- }
-
- var filtered = items.filter(function(item) {
- return !prefix || item.label.toLowerCase().indexOf(prefix.toLowerCase()) !== -1;
- });
-
- return {
- context: ctx.type,
- items: filtered.length > 0 ? filtered : items
- };
-}
-
-// ── Wikilink async completion (fetches file list from server) ─────────
-var _fileCache = null;
-var _fileCacheTime = 0;
-
-function searchWikilinks(partial, callback) {
- var now = Date.now();
- // Use cached file list if fresh (< 30s)
- if (_fileCache && (now - _fileCacheTime) < 30000) {
- callback(filterFiles(partial));
- return;
- }
-
- try {
- fetch('/api/search/advanced?q=**&vault=all&limit=500')
- .then(function(r) { return r.json(); })
- .then(function(data) {
- _fileCache = (data.results || []).map(function(r) {
- return {
- path: r.path,
- title: r.title || r.path.replace(/\.md$/i, ''),
- vault: r.vault,
- name: r.path.split('/').pop().replace(/\.md$/i, '')
- };
- });
- _fileCacheTime = now;
- callback(filterFiles(partial));
- })
- .catch(function() { callback([]); });
- } catch(e) { callback([]); }
-}
-
-function filterFiles(partial) {
- if (!_fileCache) return [];
- var q = (partial || '').toLowerCase();
- return _fileCache.filter(function(f) {
- return f.name.toLowerCase().indexOf(q) !== -1 || f.path.toLowerCase().indexOf(q) !== -1;
- }).slice(0, 20).map(function(f) {
- return {
- label: f.name,
- detail: f.path,
- insert: f.path + ']]',
- vault: f.vault
- };
- });
-}
-
-export { suggest, searchWikilinks, detectContext, MERMAID_TYPES, CODE_LANGUAGES, FRONTMATTER_FIELDS, MARKDOWN_FORMAT };
diff --git a/desktop/frontend-static/js/backup-manager.js b/desktop/frontend-static/js/backup-manager.js
deleted file mode 100644
index d546cc1..0000000
--- a/desktop/frontend-static/js/backup-manager.js
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * Backup Manager module for ObsiGate.
- *
- * Full-page view to browse, preview, restore, and delete backups across vaults.
- * Accessible from the backup/diff viewer footer.
- */
-import { api } from './auth.js';
-import { showToast } from './ui.js';
-import { safeCreateIcons } from './utils.js';
-
-var state = { backups: [], filter: '', preview: null };
-
-export function openBackupManager() {
- var area = document.getElementById('content-area');
- if (!area) return;
- area.innerHTML = '';
-
- var container = document.createElement('div');
- container.className = 'backup-manager';
- container.id = 'backup-manager';
-
- container.innerHTML =
- '' +
- '
' +
- '';
-
- area.appendChild(container);
- safeCreateIcons();
-
- document.getElementById('bm-filter').addEventListener('input', function() {
- state.filter = this.value.toLowerCase();
- render();
- });
- document.getElementById('bm-refresh').addEventListener('click', load);
- document.getElementById('bm-preview-close').addEventListener('click', function() {
- document.getElementById('bm-preview').style.display = 'none';
- });
-
- state.preview = null;
- load();
-}
-
-async function load() {
- var list = document.getElementById('bm-list');
- list.innerHTML = '';
-
- try {
- var data = await api('/api/backups');
- state.backups = data.backups || [];
- updateStats(data);
- render();
- } catch (err) {
- list.innerHTML = 'Erreur: ' + escapeHtml(err.message || 'Inconnue') + '
';
- }
-}
-
-function updateStats(data) {
- var stats = document.getElementById('bm-stats');
- if (!stats) return;
- var totalSize = formatSize(data.total_size_bytes || 0);
- stats.innerHTML =
- '' + data.total + ' backups ' +
- '' + totalSize + ' total ';
-}
-
-function render() {
- var list = document.getElementById('bm-list');
- if (!list) return;
-
- var filtered = state.backups;
- if (state.filter) {
- filtered = state.backups.filter(function(b) {
- return b.file.toLowerCase().indexOf(state.filter) !== -1 || b.vault.toLowerCase().indexOf(state.filter) !== -1;
- });
- }
-
- if (filtered.length === 0) {
- list.innerHTML = 'Aucun backup trouve.
';
- return;
- }
-
- var grouped = {};
- filtered.forEach(function(b) {
- if (!grouped[b.vault]) grouped[b.vault] = [];
- grouped[b.vault].push(b);
- });
-
- var html = '';
- var vaultNames = Object.keys(grouped).sort();
- vaultNames.forEach(function(vault) {
- var vaultBackups = grouped[vault];
- var vaultSize = vaultBackups.reduce(function(s, b) { return s + b.size; }, 0);
-
- html += '';
- html += '';
-
- var byFile = {};
- vaultBackups.forEach(function(b) {
- if (!byFile[b.file]) byFile[b.file] = [];
- byFile[b.file].push(b);
- });
-
- var fileNames = Object.keys(byFile).sort();
- fileNames.forEach(function(file) {
- var backups = byFile[file];
- backups.sort(function(a, b) { return b.timestamp - a.timestamp; });
- var totalFileSize = backups.reduce(function(s, b) { return s + b.size; }, 0);
-
- html += '
';
- html += '';
-
- html += '
';
- backups.forEach(function(b) {
- var d = new Date(b.timestamp * 1000);
- var ds = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
- html += '
';
- html += '
' + ds + ' ';
- html += '
' + formatSize(b.size) + ' ';
- html += '
';
- html += ' ';
- html += ' ';
- html += ' ';
- html += '
';
- html += '
';
- });
- html += '
';
- html += '
';
- });
-
- html += '
';
- });
-
- list.innerHTML = html;
- safeCreateIcons();
-
- // Bind vault purge
- list.querySelectorAll('.bm-vault-purge').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- if (confirm('Supprimer TOUS les backups du vault "' + btn.dataset.vault + '" ?')) {
- purgeVault(btn.dataset.vault);
- }
- });
- });
-
- // Bind file purge
- list.querySelectorAll('.bm-file-purge').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- if (confirm('Supprimer TOUS les backups de "' + btn.dataset.file + '" ?')) {
- purgeFile(btn.dataset.vault, btn.dataset.file);
- }
- });
- });
-
- // Bind backup item clicks (preview) and action buttons
- list.querySelectorAll('.bm-backup-item').forEach(function(item) {
- item.addEventListener('click', function(e) {
- // Don't trigger preview if clicking an action button
- if (e.target.closest('button')) return;
- state.preview = item.dataset.path;
- render();
- previewBackup(item.dataset.path);
- });
- });
-
- // Bind action buttons
- list.querySelectorAll('.bm-backup-preview').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- var item = btn.closest('.bm-backup-item');
- state.preview = item.dataset.path;
- render();
- previewBackup(item.dataset.path);
- });
- });
-
- list.querySelectorAll('.bm-backup-restore').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- var item = btn.closest('.bm-backup-item');
- restoreBackup(item.dataset.vault, item.dataset.file);
- });
- });
-
- list.querySelectorAll('.bm-backup-delete').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- if (confirm('Supprimer ce backup ?')) {
- deleteBackups([btn.dataset.path]);
- }
- });
- });
-}
-
-async function previewBackup(fpath) {
- var panel = document.getElementById('bm-preview');
- var content = document.getElementById('bm-preview-content');
- var title = document.getElementById('bm-preview-title');
- panel.style.display = 'block';
- content.textContent = t("common.loading");
-
- try {
- var data = await api('/api/backups/content?path=' + encodeURIComponent(fpath));
- content.textContent = data.content || '(fichier vide)';
- title.textContent = 'Apercu - ' + (data.name || '');
- } catch (err) {
- content.textContent = 'Erreur: ' + (err.message || 'Inconnue');
- }
-}
-
-async function restoreBackup(vault, file) {
- // Find the backup timestamp from the currently selected backup
- var item = document.querySelector('.bm-backup-item.active');
- if (!item) { showToast(t('backup.select_to_restore'), 'error'); return; }
-
- var backupPath = item.dataset.path;
- // Extract timestamp from the filename
- var parts = backupPath.replace(/\\/g, '/').split('/').pop().split('.');
- // The timestamp is the second-to-last component before .bak
- var ts = null;
- for (var i = parts.length - 1; i >= 0; i--) {
- if (parts[i] === 'bak' && i > 0 && /^\d+$/.test(parts[i-1])) {
- ts = parseInt(parts[i-1]);
- break;
- }
- }
- if (!ts) { showToast(t('backup.timestamp_not_found'), 'error'); return; }
-
- if (!confirm('Restaurer "' + file + '" a la version du backup selectionne ?\nLa version actuelle sera sauvegardee.')) return;
-
- try {
- var resp = await api('/api/file/' + encodeURIComponent(vault) + '/restore?path=' + encodeURIComponent(file), {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ version: ts }),
- });
- if (resp && resp.success) {
- showToast(t('backup.file_restored'), 'success');
- load();
- }
- } catch (err) {
- showToast('Erreur: ' + (err.message || 'Inconnue'), 'error');
- }
-}
-
-async function deleteBackups(paths) {
- try {
- var resp = await api('/api/backups/delete', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ paths: paths }),
- });
- showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
- load();
- } catch (err) {
- showToast('Erreur: ' + err.message, 'error');
- }
-}
-
-async function purgeFile(vault, file) {
- try {
- var resp = await api('/api/backups/purge', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ vault: vault, file: file }),
- });
- showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
- load();
- } catch (err) {
- showToast('Erreur: ' + err.message, 'error');
- }
-}
-
-async function purgeVault(vault) {
- try {
- var resp = await api('/api/backups/purge', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ vault: vault }),
- });
- showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
- load();
- } catch (err) {
- showToast('Erreur: ' + err.message, 'error');
- }
-}
-
-function formatSize(bytes) {
- if (bytes < 1024) return bytes + ' o';
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
- return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
-}
-
-function pad(n) { return String(n).padStart(2, '0'); }
-
-function escapeHtml(str) {
- var div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
-}
diff --git a/desktop/frontend-static/js/backups.js b/desktop/frontend-static/js/backups.js
deleted file mode 100644
index 7ef01a9..0000000
--- a/desktop/frontend-static/js/backups.js
+++ /dev/null
@@ -1,501 +0,0 @@
-/**
- * Backup & Diff viewer module for ObsiGate.
- *
- * Provides a modal UI to browse file backups, compare versions with
- * a unified diff view, and restore previous versions.
- *
- * Usage:
- * import { openBackupViewer } from './backups.js';
- * openBackupViewer(vaultName, filePath);
- */
-
-import { api } from './auth.js';
-import { showToast } from './ui.js';
-import { refreshSidebarTreePreservingState } from './sidebar.js';
-import { safeCreateIcons } from './utils.js';
-
-// ---------------------------------------------------------------------------
-// State
-// ---------------------------------------------------------------------------
-
-const state = {
- vault: null,
- path: null,
- backups: [],
- overlay: null,
- leftVersion: null,
- rightVersion: null,
- diffText: '',
- diffLeft: '',
- diffRight: '',
- diffMode: 'unified',
-};
-
-// ---------------------------------------------------------------------------
-// Public API
-// ---------------------------------------------------------------------------
-
-export async function openBackupViewer(vault, path) {
- state.vault = vault;
- state.path = path;
- state.backups = [];
- state.leftVersion = null;
- state.rightVersion = null;
- state.diffText = '';
- state.diffLeft = '';
- state.diffRight = '';
-
- const overlay = createOverlay();
- document.body.appendChild(overlay);
- state.overlay = overlay;
- requestAnimationFrame(function() { overlay.classList.add('active'); });
-
- setBody(renderLoading());
-
- try {
- var url = '/api/file/' + encodeURIComponent(vault) + '/backups?path=' + encodeURIComponent(path);
- var data = await api(url);
- state.backups = data.backups || [];
- } catch (err) {
- var msg = err && err.detail || err && err.message || 'Erreur inconnue';
- setBody(renderError('Erreur API: ' + msg));
- return;
- }
-
- if (state.backups.length === 0) {
- setBody(renderEmpty());
- return;
- }
-
- state.leftVersion = state.backups[0].timestamp;
- state.rightVersion = null;
- renderMainView();
- await loadDiff();
-}
-
-// ---------------------------------------------------------------------------
-// DOM helpers
-// ---------------------------------------------------------------------------
-
-var _escHandler = null;
-
-function createOverlay() {
- var overlay = document.createElement('div');
- overlay.className = 'obsigate-modal-overlay backup-viewer-overlay';
- overlay.addEventListener('click', function(e) {
- if (e.target === overlay) close();
- });
-
- var panel = document.createElement('div');
- panel.className = 'backup-viewer-panel';
- panel.id = 'backup-viewer-panel';
- overlay.appendChild(panel);
-
- // Close button -- static, never replaced by innerHTML
- var closeBtn = document.createElement('button');
- closeBtn.className = 'editor-btn backup-close-btn';
- closeBtn.id = 'backup-close';
- closeBtn.title = 'Fermer';
- closeBtn.style.cssText = 'position:absolute;top:12px;right:12px;z-index:1002;flex-shrink:0';
- closeBtn.innerHTML = ' ';
- closeBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- close();
- });
- overlay.appendChild(closeBtn);
-
- // Escape key
- _escHandler = function(e) {
- if (e.key === 'Escape' && state.overlay) close();
- };
- document.addEventListener('keydown', _escHandler);
-
- return overlay;
-}
-
-function setBody(html) {
- var panel = document.getElementById('backup-viewer-panel');
- if (panel) panel.innerHTML = html;
-}
-
-function close() {
- if (!state.overlay) return;
- if (_escHandler) {
- document.removeEventListener('keydown', _escHandler);
- _escHandler = null;
- }
- state.overlay.classList.remove('active');
- setTimeout(function() {
- if (state.overlay) state.overlay.remove();
- state.overlay = null;
- }, 200);
-}
-
-// Global click handler for close button
-document.addEventListener('click', function(e) {
- var btn = e.target.closest ? e.target.closest('#backup-close') : null;
- if (btn && state.overlay) close();
-});
-
-// ---------------------------------------------------------------------------
-// Rendering
-// ---------------------------------------------------------------------------
-
-function renderLoading() {
- return '' +
- '' +
- '
' +
- '
' +
- '
Chargement des backups...
' +
- '
';
-}
-
-function renderError(msg) {
- return '' +
- '' +
- '
' + escapeHtml(msg) + '
';
-}
-
-function renderEmpty() {
- return '' +
- '' +
- '
Aucun backup disponible pour ce fichier.
' +
- '
Les backups sont crees automatiquement avant chaque modification.
' +
- '
';
-}
-
-function renderMainView() {
- var b = state.backups;
- var leftOpts = b.map(function(bk) {
- var sel = bk.timestamp === state.leftVersion ? ' selected' : '';
- return '' + formatTimestamp(bk.timestamp) + ' (' + formatSize(bk.size) + ') ';
- }).join('');
-
- var rightOpts = ['Version actuelle ']
- .concat(b.map(function(bk) {
- var sel = bk.timestamp === state.rightVersion ? ' selected' : '';
- return '' + formatTimestamp(bk.timestamp) + ' (' + formatSize(bk.size) + ') ';
- })).join('');
-
- var html =
- '' +
-
- '' +
-
- '' +
-
- '';
-
- setBody(html);
- bindEvents();
-}
-
-// ---------------------------------------------------------------------------
-// Event bindings
-// ---------------------------------------------------------------------------
-
-function bindEvents() {
- var leftSelect = document.getElementById('backup-left-select');
- if (leftSelect) {
- leftSelect.addEventListener('change', async function() {
- state.leftVersion = parseInt(leftSelect.value);
- await loadDiff();
- });
- }
-
- var rightSelect = document.getElementById('backup-right-select');
- if (rightSelect) {
- rightSelect.addEventListener('change', async function() {
- var val = rightSelect.value;
- state.rightVersion = val === 'current' ? null : parseInt(val);
- await loadDiff();
- });
- }
-
- var restoreBtn = document.getElementById('backup-restore-btn');
- if (restoreBtn) {
- restoreBtn.addEventListener('click', async function() {
- await restoreBackup();
- });
- }
-
- var modeBtn = document.getElementById('backup-diff-mode-btn');
- var manageBtn = document.getElementById('backup-manage-btn');
- if (manageBtn) {
- manageBtn.addEventListener('click', function() {
- close();
- import('./backup-manager.js').then(function(m) { m.openBackupManager(); });
- });
- }
-
- if (modeBtn) {
- modeBtn.addEventListener('click', function() {
- state.diffMode = state.diffMode === 'sidebyside' ? 'unified' : 'sidebyside';
- var container = document.getElementById('backup-diff-container');
- if (container) {
- container.innerHTML = state.diffMode === 'sidebyside'
- ? renderDiffSideBySide(state.diffLeft, state.diffRight)
- : renderDiff(state.diffText);
- }
- var icon = modeBtn.querySelector('i');
- if (icon) icon.setAttribute('data-lucide', state.diffMode === 'sidebyside' ? 'columns-2' : 'align-justify');
- var textNodes = [];
- for (var i = 0; i < modeBtn.childNodes.length; i++) {
- if (modeBtn.childNodes[i].nodeType === 3) textNodes.push(modeBtn.childNodes[i]);
- }
- textNodes.forEach(function(n) { n.remove(); });
- modeBtn.appendChild(document.createTextNode(state.diffMode === 'sidebyside' ? ' Unifiee' : ' Cote-a-cote'));
- safeCreateIcons();
- });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Data loading
-// ---------------------------------------------------------------------------
-
-async function loadDiff() {
- var container = document.getElementById('backup-diff-container');
- if (!container) return;
-
- container.innerHTML =
- '';
-
- var url = '/api/file/' + encodeURIComponent(state.vault) + '/diff?path=' + encodeURIComponent(state.path) + '&version=' + state.leftVersion;
- if (state.rightVersion !== null) {
- url += '&compare_with=' + state.rightVersion;
- }
-
- try {
- var data = await api(url);
- state.diffText = data.diff || '';
- state.diffLeft = data.left_content || '';
- state.diffRight = data.right_content || '';
- container.innerHTML = state.diffMode === 'sidebyside'
- ? renderDiffSideBySide(state.diffLeft, state.diffRight)
- : renderDiff(state.diffText);
- } catch (err) {
- container.innerHTML = 'Erreur: ' + escapeHtml((err && err.detail) || (err && err.message) || 'Unknown') + '
';
- }
-}
-
-async function restoreBackup() {
- if (!state.leftVersion) return;
-
- var confirmed = confirm(
- 'Restaurer "' + state.path + '" a la version du ' + formatTimestamp(state.leftVersion) + ' ?\n\n' +
- 'La version actuelle sera sauvegardee avant la restauration.'
- );
- if (!confirmed) return;
-
- var restoreBtn = document.getElementById('backup-restore-btn');
- if (restoreBtn) {
- restoreBtn.disabled = true;
- restoreBtn.textContent = 'Restauration...';
- }
-
- try {
- var resp = await api(
- '/api/file/' + encodeURIComponent(state.vault) + '/restore?path=' + encodeURIComponent(state.path),
- {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ version: state.leftVersion }),
- }
- );
-
- if (resp && resp.success) {
- showToast(t('backup.file_restored'), 'success');
- await refreshSidebarTreePreservingState();
- close();
- if (typeof TabManager !== 'undefined' && TabManager.reloadCurrent) {
- TabManager.reloadCurrent();
- }
- } else {
- showToast(t('backup.restore_failed'), 'error');
- }
- } catch (err) {
- showToast('Erreur: ' + ((err && err.detail) || (err && err.message) || 'Unknown'), 'error');
- } finally {
- if (restoreBtn) {
- restoreBtn.disabled = false;
- restoreBtn.innerHTML = ' Restaurer';
- safeCreateIcons();
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-// Diff rendering
-// ---------------------------------------------------------------------------
-
-function renderDiff(diffText) {
- if (!diffText) {
- return 'Aucune difference - les versions sont identiques.
';
- }
-
- var html = '';
- var lines = diffText.split('\n');
-
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i];
- var cssClass = 'diff-context';
- var prefix = ' ';
- if (line.indexOf('@@') === 0) { cssClass = 'diff-hunk'; prefix = ''; }
- else if (line.indexOf('+') === 0) { cssClass = 'diff-add'; prefix = '+'; }
- else if (line.indexOf('-') === 0) { cssClass = 'diff-del'; prefix = '-'; }
- else if (line.indexOf('---') === 0 || line.indexOf('+++') === 0) { cssClass = 'diff-header-line'; prefix = ''; }
-
- html += '' + prefix + ' ' + escapeHtml(line) + ' ';
- }
-
- html += '
';
- return html;
-}
-
-// ---------------------------------------------------------------------------
-// Side-by-side diff rendering
-// ---------------------------------------------------------------------------
-
-function renderDiffSideBySide(leftText, rightText) {
- if (!leftText && !rightText) {
- return 'Aucune difference - les versions sont identiques.
';
- }
-
- var leftLines = (leftText || '').split('\n');
- var rightLines = (rightText || '').split('\n');
- var sm = new DifflibSeqMatcher(leftLines, rightLines);
- var ops = sm.getOpcodes();
-
- var html = '';
-
- for (var oi = 0; oi < ops.length; oi++) {
- var op = ops[oi];
- var tag = op[0], i1 = op[1], i2 = op[2], j1 = op[3], j2 = op[4];
-
- if (tag === 'equal') {
- for (var k = 0; k < (i2 - i1); k++) {
- html += '' + escapeHtml(leftLines[i1 + k]) + ' ' + escapeHtml(rightLines[j1 + k]) + ' ';
- }
- } else if (tag === 'delete') {
- for (var k2 = i1; k2 < i2; k2++) {
- html += '' + escapeHtml(leftLines[k2]) + ' ';
- }
- } else if (tag === 'insert') {
- for (var k3 = j1; k3 < j2; k3++) {
- html += '' + escapeHtml(rightLines[k3]) + ' ';
- }
- } else if (tag === 'replace') {
- var maxLen = Math.max(i2 - i1, j2 - j1);
- for (var k4 = 0; k4 < maxLen; k4++) {
- var leftLine = (i1 + k4 < i2) ? escapeHtml(leftLines[i1 + k4]) : '';
- var rightLine = (j1 + k4 < j2) ? escapeHtml(rightLines[j1 + k4]) : '';
- var leftClass = (i1 + k4 < i2) ? 'diff-del' : 'diff-empty-cell';
- var rightClass = (j1 + k4 < j2) ? 'diff-add' : 'diff-empty-cell';
- html += '' + leftLine + ' ' + rightLine + ' ';
- }
- }
- }
-
- html += '
';
- return html;
-}
-
-// Minimal SequenceMatcher for line-based diff
-function DifflibSeqMatcher(a, b) {
- this.a = a;
- this.b = b;
-}
-
-DifflibSeqMatcher.prototype.getOpcodes = function() {
- var m = this.a.length, n = this.b.length;
- var dp = [];
- for (var i = 0; i <= m; i++) { dp[i] = []; for (var j = 0; j <= n; j++) dp[i][j] = 0; }
- for (var i = 1; i <= m; i++) {
- for (var j = 1; j <= n; j++) {
- if (this.a[i - 1] === this.b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
- else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
- }
- }
- var ops = [];
- var i = m, j = n;
- while (i > 0 || j > 0) {
- if (i > 0 && j > 0 && this.a[i - 1] === this.b[j - 1]) {
- var ei = i - 1, ej = j - 1;
- while (ei > 0 && ej > 0 && this.a[ei - 1] === this.b[ej - 1]) { ei--; ej--; }
- if (ei < i) ops.unshift(['equal', ei, i, ej, j]);
- i = ei; j = ej;
- } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
- ops.unshift(['insert', i, i, j - 1, j]);
- j--;
- } else if (i > 0 && (j === 0 || dp[i][j - 1] < dp[i - 1][j])) {
- ops.unshift(['delete', i - 1, i, j, j]);
- i--;
- }
- }
- var merged = [];
- for (var k = 0; k < ops.length; k++) {
- if (merged.length > 0 && merged[merged.length - 1][0] === 'delete' && ops[k][0] === 'insert' &&
- merged[merged.length - 1][4] === ops[k][3]) {
- var del = merged.pop();
- merged.push(['replace', del[1], del[2], ops[k][3], ops[k][4]]);
- } else {
- merged.push(ops[k]);
- }
- }
- return merged;
-};
-
-// ---------------------------------------------------------------------------
-// Utility
-// ---------------------------------------------------------------------------
-
-function formatTimestamp(ts) {
- var d = new Date(ts * 1000);
- var pad = function(n) { return String(n).padStart(2, '0'); };
- return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
-}
-
-function formatSize(bytes) {
- if (bytes < 1024) return bytes + ' o';
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
- return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
-}
-
-function escapeHtml(str) {
- var div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
-}
diff --git a/desktop/frontend-static/js/config.js b/desktop/frontend-static/js/config.js
deleted file mode 100644
index c06a0f4..0000000
--- a/desktop/frontend-static/js/config.js
+++ /dev/null
@@ -1,1570 +0,0 @@
-// config.js — extracted from app.js (3872-4865)
-import { api, AuthManager } from './auth.js';
-import { state } from './state.js';
-import { el, icon, openFile } from './viewer.js';
-import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadVaultSettings, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js';
-import { escapeHtml, safeCreateIcons } from './utils.js';
-import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
-import { t, setLocale, getLocale } from './i18n.js';
-
-let _recentTimestampTimer = null;
-let _recentFilesCache = [];
-let _recentRefreshTimer = null;
-
-export async function loadRecentFiles(vaultFilter) {
- const listEl = document.getElementById("recent-list");
- const emptyEl = document.getElementById("recent-empty");
- if (!listEl) return;
-
- let url = "/api/recent?mode=modified";
- if (vaultFilter) url += `&vault=${encodeURIComponent(vaultFilter)}`;
- try {
- const data = await api(url);
- _recentFilesCache = data.files || [];
- renderRecentList(_recentFilesCache);
- } catch (err) {
- console.error("Failed to load recent files:", err);
- listEl.innerHTML = "";
- if (emptyEl) {
- emptyEl.classList.remove("hidden");
- }
- }
-}
-
-function renderRecentList(files) {
- const listEl = document.getElementById("recent-list");
- const emptyEl = document.getElementById("recent-empty");
- if (!listEl) return;
- listEl.innerHTML = "";
-
- if (!files || files.length === 0) {
- if (emptyEl) {
- emptyEl.classList.remove("hidden");
- safeCreateIcons();
- }
- return;
- }
- if (emptyEl) emptyEl.classList.add("hidden");
-
- files.forEach((f) => {
- const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path });
-
- // Header row: time + vault badge
- const header = el("div", { class: "recent-item-header" });
- const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]);
- const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]);
- header.appendChild(timeSpan);
- header.appendChild(badge);
- item.appendChild(header);
-
- // Title
- const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]);
- item.appendChild(titleEl);
-
- // Path breadcrumb
- const pathParts = f.path.split("/");
- if (pathParts.length > 1) {
- const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]);
- item.appendChild(pathEl);
- }
-
- // Preview
- if (f.preview) {
- const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]);
- item.appendChild(previewEl);
- }
-
- // Tags
- if (f.tags && f.tags.length > 0) {
- const tagsEl = el("div", { class: "recent-item-tags" });
- f.tags.forEach((t) => {
- tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)]));
- });
- item.appendChild(tagsEl);
- }
-
- // Click handler
- item.addEventListener("click", () => {
- openFile(f.vault, f.path);
- closeMobileSidebar();
- });
-
- listEl.appendChild(item);
- });
- safeCreateIcons();
-}
-
-function _humanizeDelta(mtime) {
- const delta = Date.now() / 1000 - mtime;
- if (delta < 60) return t("config.just_now");
- if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
- if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
- if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
- return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
-}
-
-function _refreshRecentTimestamps() {
- if (state.activeSidebarTab !== "recent" || !_recentFilesCache.length) return;
- const items = document.querySelectorAll(".recent-item");
- items.forEach((item, i) => {
- if (i < _recentFilesCache.length) {
- const timeSpan = item.querySelector(".recent-time");
- if (timeSpan) {
- // keep the icon, update text
- const textNode = timeSpan.lastChild;
- if (textNode && textNode.nodeType === Node.TEXT_NODE) {
- textNode.textContent = _humanizeDelta(_recentFilesCache[i].mtime);
- }
- }
- }
- });
-}
-
-export function _populateRecentVaultFilter() {
- const select = document.getElementById("recent-vault-filter");
- if (!select) return;
- // keep first option "Tous les vaults"
- while (select.options.length > 1) select.remove(1);
- state.allVaults.forEach((v) => {
- const opt = document.createElement("option");
- opt.value = v.name;
- opt.textContent = v.name;
- select.appendChild(opt);
- });
- syncVaultSelectors();
-}
-
-function initRecentTab() {
- const select = document.getElementById("recent-vault-filter");
- if (select) {
- select.addEventListener("change", async () => {
- const val = select.value || "all";
- await setSelectedVaultContext(val, { focusVault: val !== "all" });
- });
- }
- // Periodic timestamp refresh (every 60s)
- _recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
-}
-
-// ---------------------------------------------------------------------------
-// Sidebar tabs
-// ---------------------------------------------------------------------------
-function initSidebarTabs() {
- document.querySelectorAll(".sidebar-tab").forEach((tab) => {
- tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
- });
-}
-
-function switchSidebarTab(tab) {
- state.activeSidebarTab = tab;
- document.querySelectorAll(".sidebar-tab").forEach((btn) => {
- const isActive = btn.dataset.tab === tab;
- btn.classList.toggle("active", isActive);
- btn.setAttribute("aria-selected", isActive ? "true" : "false");
- });
- document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
- const isActive = panel.id === `sidebar-panel-${tab}`;
- panel.classList.toggle("active", isActive);
- });
- const filterInput = document.getElementById("sidebar-filter-input");
- if (filterInput) {
- const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
- filterInput.placeholder = placeholders[tab] || "";
- }
- const query = filterInput ? (state.sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : "";
- if (query) {
- if (tab === "vaults") performTreeSearch(query);
- else if (tab === "tags") filterTagCloud(query);
- }
- // Auto-load recent files when switching to the recent tab
- if (tab === "recent") {
- _populateRecentVaultFilter();
- const vaultFilter = document.getElementById("recent-vault-filter");
- loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
- }
-}
-
-function initHelpModal() {
- const openBtn = document.getElementById("help-open-btn");
- const closeBtn = document.getElementById("help-close");
- const modal = document.getElementById("help-modal");
- if (!openBtn || !closeBtn || !modal) return;
-
- let helpNavInitialized = false;
-
- openBtn.addEventListener("click", () => {
- modal.classList.add("active");
- closeHeaderMenu();
- safeCreateIcons();
- // Reset TOC to CSS default (mobile: hidden, desktop: visible)
- const nav = document.getElementById("help-nav");
- if (nav) nav.style.display = '';
- if (!helpNavInitialized) {
- initHelpNavigation();
- helpNavInitialized = true;
- }
- });
-
- closeBtn.addEventListener("click", closeHelpModal);
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- closeHelpModal();
- }
- });
-
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeHelpModal();
- }
- });
-}
-
-function initEditorPocBtn() {
- var btn = document.getElementById("editor-poc-btn");
- if (!btn) return;
- btn.addEventListener("click", function() {
- window.open("/editor-poc", "_blank");
- });
-}
-
-function initHelpNavigation() {
- const helpBody = document.getElementById("help-body");
- const navList = document.getElementById("help-nav-list");
- const nav = document.getElementById("help-nav");
- const searchInput = document.getElementById("help-nav-search");
- const helpContent = document.querySelector(".help-content");
-
- if (!helpBody || !navList || !nav) return;
-
- const allLinks = [...navList.querySelectorAll(".help-nav-link")];
- const allSections = [...document.querySelectorAll(".help-section")];
- const sectionMap = {};
- allSections.forEach(s => { sectionMap[s.id] = s; });
-
- // Build text index for content search
- const sectionTextCache = {};
- allSections.forEach(s => {
- sectionTextCache[s.id] = (s.textContent || '').toLowerCase();
- });
-
- // ── Mobile: simple display toggle ──
- function isMobile() { return window.innerWidth <= 768; }
- function toggleTOC() {
- if (nav.style.display === 'none' || nav.style.display === '') {
- nav.style.display = 'flex';
- if (searchInput) setTimeout(() => searchInput.focus(), 150);
- } else {
- nav.style.display = 'none';
- }
- }
-
- // ── Scroll to section ──
- function scrollToSection(targetId) {
- const section = sectionMap[targetId];
- if (!section) return;
- section.scrollIntoView({ behavior: "smooth", block: "start" });
- section.setAttribute("tabindex", "-1");
- if (isMobile()) nav.style.display = 'none';
- }
-
- // ── Focus the content pane ──
- function focusContent() {
- const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
- if (activeLink) {
- const targetId = activeLink.getAttribute("href").substring(1);
- const section = sectionMap[targetId];
- if (section) {
- const h2 = section.querySelector("h2, h3");
- if (h2) { h2.setAttribute("tabindex", "-1"); h2.focus({ preventScroll: true }); return; }
- }
- }
- if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.focus({ preventScroll: true }); }
- }
-
- // ── Focus TOC active link ──
- function focusTOC() {
- if (isMobile() && (nav.style.display === 'none' || nav.style.display === '')) {
- nav.style.display = 'flex';
- }
- const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
- if (activeLink) { activeLink.focus(); return; }
- const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
- if (firstLink) firstLink.focus();
- }
-
- // ── Nav link click handler ──
- allLinks.forEach((link) => {
- link.addEventListener("click", (e) => {
- e.preventDefault();
- const targetId = link.getAttribute("href").substring(1);
- scrollToSection(targetId);
- });
- });
-
- // ── Keyboard navigation inside nav list ──
- navList.addEventListener("keydown", (e) => {
- const items = [...navList.querySelectorAll(".help-nav-link:not(.hidden)")];
- if (!items.length) return;
- const currentIdx = items.indexOf(document.activeElement);
-
- if (e.key === "ArrowDown") {
- e.preventDefault();
- const next = (currentIdx + 1) % items.length;
- items[next].focus();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- const prev = (currentIdx - 1 + items.length) % items.length;
- items[prev].focus();
- } else if (e.key === "ArrowRight") {
- e.preventDefault();
- focusContent();
- } else if (e.key === "ArrowLeft") {
- e.preventDefault();
- const prev = (currentIdx - 1 + items.length) % items.length;
- items[prev].focus();
- } else if (e.key === "Home") {
- e.preventDefault();
- items[0].focus();
- } else if (e.key === "End") {
- e.preventDefault();
- items[items.length - 1].focus();
- } else if (e.key === "Enter" && currentIdx >= 0) {
- e.preventDefault();
- const targetId = items[currentIdx].getAttribute("href").substring(1);
- scrollToSection(targetId);
- setTimeout(() => focusContent(), 400);
- }
- });
-
- navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
-
- // ── Keyboard navigation in content pane ──
- if (helpContent) {
- helpContent.setAttribute("tabindex", "-1");
- helpContent.addEventListener("keydown", (e) => {
- if (e.key === "ArrowLeft") {
- e.preventDefault();
- focusTOC();
- } else if (e.key === "ArrowUp" && document.activeElement === helpContent) {
- e.preventDefault();
- focusTOC();
- }
- });
- }
-
- // ── Keyboard navigation in search input ──
- if (searchInput) {
- searchInput.addEventListener("keydown", (e) => {
- if (e.key === "ArrowDown") {
- e.preventDefault();
- const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
- if (firstLink) firstLink.focus();
- }
- });
- }
-
- // ── Search: filter TOC + hide non-matching sections + highlight text ──
- let currentQuery = "";
- if (searchInput) {
- searchInput.addEventListener("input", () => {
- currentQuery = searchInput.value.toLowerCase().trim();
- applySearchFilter();
- });
- searchInput.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && currentQuery) {
- searchInput.value = "";
- currentQuery = "";
- applySearchFilter();
- }
- });
- }
-
- function clearHighlights() {
- allSections.forEach(section => {
- section.querySelectorAll(".help-search-highlight").forEach(el => {
- const parent = el.parentNode;
- parent.replaceChild(document.createTextNode(el.textContent), el);
- parent.normalize();
- });
- });
- }
-
- function highlightText(root, query) {
- if (!query || query.length < 2) return;
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
- const textNodes = [];
- while (walker.nextNode()) {
- const node = walker.currentNode;
- // Skip nodes inside script, style, or already-highlighted
- if (node.parentElement && !node.parentElement.closest("script, style, .help-search-highlight, .help-nav")) {
- textNodes.push(node);
- }
- }
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
- textNodes.forEach(node => {
- const text = node.textContent;
- if (!regex.test(text)) return;
- regex.lastIndex = 0;
- const frag = document.createDocumentFragment();
- let lastIdx = 0;
- let match;
- while ((match = regex.exec(text)) !== null) {
- if (match.index > lastIdx) {
- frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)));
- }
- const mark = document.createElement("mark");
- mark.className = "help-search-highlight";
- mark.textContent = match[0];
- frag.appendChild(mark);
- lastIdx = regex.lastIndex;
- if (match[0].length === 0) regex.lastIndex++;
- }
- if (lastIdx < text.length) {
- frag.appendChild(document.createTextNode(text.slice(lastIdx)));
- }
- node.parentNode.replaceChild(frag, node);
- });
- }
-
- function applySearchFilter() {
- const q = currentQuery;
- // Clear previous highlights
- clearHighlights();
- // Filter TOC links
- allLinks.forEach((link) => {
- const text = link.textContent.toLowerCase();
- if (!q || text.includes(q)) {
- link.classList.remove("hidden");
- } else {
- link.classList.add("hidden");
- }
- });
- // Filter content sections + highlight
- allSections.forEach((section) => {
- if (!q) {
- section.classList.remove("help-hidden");
- return;
- }
- const sectionText = sectionTextCache[section.id] || '';
- const sectionTitle = (section.querySelector("h2, h3")?.textContent || '').toLowerCase();
- if (sectionTitle.includes(q) || sectionText.includes(q)) {
- section.classList.remove("help-hidden");
- // Highlight matching text in this section
- highlightText(section, q);
- } else {
- section.classList.add("help-hidden");
- }
- });
- updateActiveLink();
- ensureNoResultsMessage(q);
- }
-
- function ensureNoResultsMessage(q) {
- const existing = document.getElementById("help-no-results");
- const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
- if (q && visibleSections.length === 0) {
- if (!existing) {
- const msg = document.createElement("div");
- msg.id = "help-no-results";
- msg.style.cssText = "padding:40px;text-align:center;color:var(--text-muted);font-size:0.9rem";
- msg.innerHTML = `🔍
Aucun résultat pour "${q}"
Essayez un autre terme
`;
- if (helpContent) helpContent.appendChild(msg);
- }
- } else if (existing) {
- existing.remove();
- }
- }
-
- // ── Scroll spy ──
- function updateActiveLink() {
- const visibleLinks = allLinks.filter(l => !l.classList.contains("hidden"));
- const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
- let activeId = null;
- for (const section of visibleSections) {
- const rect = section.getBoundingClientRect();
- const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
- if (rect.top < (contentRect ? contentRect.top + 120 : 200)) {
- activeId = section.id;
- }
- }
- visibleLinks.forEach((link) => {
- const isMatch = link.getAttribute("href") === `#${activeId}`;
- link.classList.toggle("active", isMatch);
- });
- }
-
- const observer = new IntersectionObserver(
- (entries) => {
- let bestEntry = null;
- for (const entry of entries) {
- if (entry.isIntersecting) {
- if (!bestEntry || entry.boundingClientRect.top < bestEntry.boundingClientRect.top) {
- bestEntry = entry;
- }
- }
- }
- if (bestEntry) {
- const id = bestEntry.target.getAttribute("id");
- allLinks.forEach((link) => {
- const isMatch = link.getAttribute("href") === `#${id}`;
- link.classList.toggle("active", isMatch);
- });
- }
- },
- {
- root: helpBody,
- rootMargin: "-10% 0px -75% 0px",
- threshold: [0, 0.25, 0.5],
- }
- );
-
- allSections.forEach((section) => observer.observe(section));
-
- // ── Keyboard: Ctrl+↑/↓ to jump between sections ──
- helpBody.addEventListener("keydown", (e) => {
- if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
- e.preventDefault();
- const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
- const currentSection = visibleSections.find((s) => {
- const rect = s.getBoundingClientRect();
- const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
- return rect.top >= (contentRect ? contentRect.top : 0) && rect.top < (contentRect ? contentRect.top + 120 : 120);
- });
- if (!currentSection) return;
- const currentIdx = visibleSections.indexOf(currentSection);
- if (e.key === "ArrowDown" && currentIdx < visibleSections.length - 1) {
- visibleSections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" });
- } else if (e.key === "ArrowUp" && currentIdx > 0) {
- visibleSections[currentIdx - 1].scrollIntoView({ behavior: "smooth", block: "start" });
- }
- }
- });
-}
-
-function closeHelpModal() {
- const modal = document.getElementById("help-modal");
- if (modal) modal.classList.remove("active");
-}
-
-function initConfigModal() {
- const openBtn = document.getElementById("config-open-btn");
- const closeBtn = document.getElementById("config-close");
- const modal = document.getElementById("config-modal");
- const addBtn = document.getElementById("config-add-btn");
- const patternInput = document.getElementById("config-pattern-input");
-
- if (!openBtn || !closeBtn || !modal) return;
-
- openBtn.addEventListener("click", async () => {
- modal.classList.add("active");
- closeHeaderMenu();
- renderConfigFilters();
- loadConfigFields();
- loadDiagnostics();
- loadAbout();
- await loadHiddenFilesSettings();
- loadWebhooksUI();
- loadSharesUI();
- safeCreateIcons();
- });
-
- closeBtn.addEventListener("click", closeConfigModal);
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- closeConfigModal();
- }
- });
-
- addBtn.addEventListener("click", addConfigFilter);
- patternInput.addEventListener("keypress", (e) => {
- if (e.key === "Enter") {
- addConfigFilter();
- }
- });
-
- patternInput.addEventListener("input", updateRegexPreview);
-
- // Frontend config fields — save to localStorage on change
- ["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => {
- const input = document.getElementById(id);
- if (input) input.addEventListener("change", saveFrontendConfig);
- });
-
- // Backend save button
- const saveBtn = document.getElementById("cfg-save-backend");
- if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig);
-
- // Force reindex
- const reindexBtn = document.getElementById("cfg-reindex");
- if (reindexBtn) reindexBtn.addEventListener("click", forceReindex);
-
- // Reset defaults
- const resetBtn = document.getElementById("cfg-reset-defaults");
- if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults);
-
- // Refresh diagnostics
- const diagBtn = document.getElementById("cfg-refresh-diag");
- if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics);
-
- // Hidden files configuration
- const saveHiddenBtn = document.getElementById("cfg-save-hidden-files");
- if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings);
-
- // AI Keys
- const saveAIKeysBtn = document.getElementById("cfg-save-ai-keys");
- if (saveAIKeysBtn) saveAIKeysBtn.addEventListener("click", saveAIKeys);
- const testAIKeysBtn = document.getElementById("cfg-test-ai-keys");
- if (testAIKeysBtn) testAIKeysBtn.addEventListener("click", testAIKeys);
- // Load existing keys on modal open
- document.getElementById("config-open-btn").addEventListener("click", loadAIKeys);
-
- // Profile menu button: open config modal and scroll to profile section
- var profileBtn = document.getElementById("profile-open-btn");
- if (profileBtn) {
- profileBtn.addEventListener("click", function() {
- var modal = document.getElementById("config-modal");
- if (modal) modal.classList.add("active");
- setTimeout(function() {
- var el = document.getElementById("cfg-profile");
- if (el) el.scrollIntoView({ behavior: 'smooth' });
- }, 300);
- });
- }
-
- // Logout button — handled inline in index.html (onclick)
-
- // Config nav search
- var cfgSearch = document.getElementById("config-nav-search");
- if (cfgSearch) {
- cfgSearch.addEventListener("input", function() {
- var q = cfgSearch.value.toLowerCase();
- document.querySelectorAll("#config-scroll .config-section").forEach(function(s) {
- var text = s.textContent.toLowerCase();
- s.style.display = (q && !text.includes(q)) ? "none" : "";
- });
- document.querySelectorAll("#config-nav .help-nav-link").forEach(function(a) {
- var li = a.closest("li");
- if (li) li.style.display = (q && !a.textContent.toLowerCase().includes(q)) ? "none" : "";
- });
- });
- }
-
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeConfigModal();
- }
- });
-
- // Load saved frontend config on startup
- applyFrontendConfig();
-
- // Init theme picker
- initThemePicker();
-
- // Init profile
- initProfile();
-}
-
-function closeConfigModal() {
- const modal = document.getElementById("config-modal");
- if (modal) modal.classList.remove("active");
-}
-
-// --- Config field helpers ---
-const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
-
-function _getFrontendConfig() {
- try {
- return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
- } catch {
- return {};
- }
-}
-
-function applyFrontendConfig() {
- const cfg = _getFrontendConfig();
- if (cfg.debounce_ms) {
- /* applied dynamically in debounce setTimeout */
- }
- if (cfg.results_per_page) {
- /* used as ADVANCED_SEARCH_LIMIT override */
- }
- if (cfg.min_query_length) {
- /* used as MIN_SEARCH_LENGTH override */
- }
- if (cfg.search_timeout_ms) {
- /* used as SEARCH_TIMEOUT_MS override */
- }
-}
-
-export function _getEffective(key, fallback) {
- const cfg = _getFrontendConfig();
- return cfg[key] !== undefined ? cfg[key] : fallback;
-}
-
-async function loadConfigFields() {
- // Frontend fields from localStorage
- const cfg = _getFrontendConfig();
- _setField("cfg-debounce", cfg.debounce_ms || 300);
- _setField("cfg-results-per-page", cfg.results_per_page || 50);
- _setField("cfg-min-query", cfg.min_query_length || 2);
- _setField("cfg-timeout", cfg.search_timeout_ms || 30000);
-
- // Backend fields from API
- try {
- const data = await api("/api/config");
- _setField("cfg-workers", data.search_workers);
- _setField("cfg-max-content", data.max_content_size);
- _setField("cfg-title-boost", data.title_boost);
- _setField("cfg-tag-boost", data.tag_boost);
- _setField("cfg-prefix-exp", data.prefix_max_expansions);
- _setField("cfg-recent-limit", data.recent_files_limit || 20);
- _setField("cfg-max-backups", data.max_backups_per_file || 10);
- // Watcher config
- _setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
- _setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
- _setField("cfg-watcher-interval", data.watcher_polling_interval || 5);
- _setField("cfg-watcher-debounce", data.watcher_debounce || 2);
- } catch (err) {
- console.error("Failed to load backend config:", err);
- }
-}
-
-function _setField(id, value) {
- const el = document.getElementById(id);
- if (el && value !== undefined) el.value = value;
-}
-
-function _setCheckbox(id, checked) {
- const el = document.getElementById(id);
- if (el) el.checked = !!checked;
-}
-
-function _getCheckbox(id) {
- const el = document.getElementById(id);
- return el ? el.checked : false;
-}
-
-function _getFieldNum(id, fallback) {
- const el = document.getElementById(id);
- if (!el) return fallback;
- const v = parseFloat(el.value);
- return isNaN(v) ? fallback : v;
-}
-
-function saveFrontendConfig() {
- const cfg = {
- debounce_ms: _getFieldNum("cfg-debounce", 300),
- results_per_page: _getFieldNum("cfg-results-per-page", 50),
- min_query_length: _getFieldNum("cfg-min-query", 2),
- search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
- };
- localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
- showToast(t("toast.config_client_saved"), "success");
-}
-
-async function saveBackendConfig() {
- const body = {
- search_workers: _getFieldNum("cfg-workers", 2),
- max_content_size: _getFieldNum("cfg-max-content", 100000),
- title_boost: _getFieldNum("cfg-title-boost", 3.0),
- tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
- prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
- recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
- max_backups_per_file: _getFieldNum("cfg-max-backups", 10),
- watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
- watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
- watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
- watcher_debounce: _getFieldNum("cfg-watcher-debounce", 2.0),
- };
- try {
- const res = await fetch("/api/config", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- if (res.ok) {
- showToast(t("toast.config_saved"), "success");
- } else {
- const errorData = await res.json().catch(() => ({}));
- showToast(errorData.detail || "Erreur de sauvegarde", "error");
- }
- } catch (err) {
- console.error("Failed to save backend config:", err);
- showToast(t("config.save_error"), "error");
- }
-}
-
-async function forceReindex() {
- const btn = document.getElementById("cfg-reindex");
- if (btn) {
- btn.disabled = true;
- btn.textContent = t('config.reindexing');
- }
- try {
- await api("/api/index/reload");
- showToast(t("toast.reindex_done"), "success");
- loadDiagnostics();
- await Promise.all([loadVaults(), loadTags()]);
- } catch (err) {
- console.error("Reindex error:", err);
- showToast(t("config.reindex_error"), "error");
- } finally {
- if (btn) {
- btn.disabled = false;
- btn.textContent = t('config.force_reindex');
- }
- }
-}
-
-async function resetConfigDefaults() {
- // Reset frontend
- localStorage.removeItem(_FRONTEND_CONFIG_KEY);
- // Reset backend
- try {
- await fetch("/api/config", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- search_workers: 2,
- debounce_ms: 300,
- results_per_page: 50,
- min_query_length: 2,
- search_timeout_ms: 30000,
- max_content_size: 100000,
- title_boost: 3.0,
- path_boost: 1.5,
- tag_boost: 2.0,
- prefix_max_expansions: 50,
- snippet_context_chars: 120,
- max_snippet_highlights: 5,
- }),
- });
- } catch (err) {
- console.error("Reset config error:", err);
- }
- loadConfigFields();
- showToast(t("toast.config_reset"), "success");
-}
-
-async function loadDiagnostics() {
- const container = document.getElementById("config-diagnostics");
- if (!container) return;
- container.innerHTML = 'Chargement...
';
- try {
- const data = await api("/api/diagnostics");
- renderDiagnostics(container, data);
- } catch (err) {
- container.innerHTML = 'Erreur de chargement
';
- }
-}
-
-function renderDiagnostics(container, data) {
- container.innerHTML = "";
- const sections = [
- {
- title: "Index",
- rows: [
- [t("about.files_indexed"), data.index.total_files],
- [t("dashboard.stats_tags"), data.index.total_tags],
- [t("dashboard.stats_vaults"), Object.keys(data.index.vaults).join(", ")],
- ],
- },
- {
- title: "Index inversé",
- rows: [
- ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
- ["Postings total", data.inverted_index.total_postings.toLocaleString()],
- ["Documents", data.inverted_index.documents],
- ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
- ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
- ],
- },
- {
- title: "Moteur de recherche",
- rows: [
- ["Executor actif", data.search_executor.active ? "Oui" : "Non"],
- ["Workers max", data.search_executor.max_workers],
- ],
- },
- ];
- sections.forEach((section) => {
- const div = document.createElement("div");
- div.className = "config-diag-section";
- const title = document.createElement("div");
- title.className = "config-diag-section-title";
- title.textContent = section.title;
- div.appendChild(title);
- section.rows.forEach(([label, value]) => {
- const row = document.createElement("div");
- row.className = "config-diag-row";
- row.innerHTML = `${label} ${value} `;
- div.appendChild(row);
- });
- container.appendChild(div);
- });
-}
-
-// --- About Section ---
-
-function loadAbout() {
- const container = document.getElementById("config-about");
- if (!container) return;
-
- // Fetch health info for version
- api("/api/health").then((health) => {
- container.innerHTML = "";
-
- const sections = [
- {
- title: t("about.section_app"),
- rows: [
- [t("about.name"), "ObsiGate"],
- [t("about.version"), state.APP_VERSION],
- [t("about.api_version"), health.version || "—"],
- [t("about.status"), health.status || "—"],
- ],
- },
- {
- title: t("about.section_env"),
- rows: [
- [t("about.vaults_configured"), health.vaults || "—"],
- [t("about.files_indexed"), health.total_files || "—"],
- [t("about.browser"), navigator.userAgent.split(" ").pop()],
- [t("about.platform"), navigator.platform || "—"],
- [t("about.language"), navigator.language || "—"],
- ],
- },
- {
- title: t("about.section_components"),
- rows: [
- [t("about.backend"), "FastAPI (Python)"],
- [t("about.markdown_renderer"), "mistune"],
- [t("about.file_watcher"), "watchdog"],
- [t("about.frontend"), "Vanilla JavaScript"],
- [t("about.icons"), "Lucide Icons"],
- [t("about.syntax_highlight"), "highlight.js"],
- [t("about.editor"), "CodeMirror 6"],
- ],
- },
- ];
-
- sections.forEach((section) => {
- const div = document.createElement("div");
- div.className = "config-diag-section";
- const title = document.createElement("div");
- title.className = "config-diag-section-title";
- title.textContent = section.title;
- div.appendChild(title);
- section.rows.forEach(([label, value]) => {
- const row = document.createElement("div");
- row.className = "config-diag-row";
- row.innerHTML = `${label} ${value} `;
- div.appendChild(row);
- });
- container.appendChild(div);
- });
- }).catch(() => {
- container.innerHTML = 'Erreur de chargement
';
- });
-}
-
-// --- Hidden Files Configuration ---
-
-async function loadHiddenFilesSettings() {
- const container = document.getElementById("hidden-files-vault-list");
- if (!container) return;
-
- container.innerHTML = 'Chargement...
';
-
- try {
- const settings = await api("/api/vaults/settings/all");
- renderHiddenFilesSettings(container, settings);
- } catch (err) {
- console.error("Failed to load hidden files settings:", err);
- container.innerHTML = 'Erreur de chargement
';
- }
-}
-
-function renderHiddenFilesSettings(container, allSettings) {
- container.innerHTML = "";
-
- if (!state.allVaults || state.allVaults.length === 0) {
- container.innerHTML = 'Aucun vault configuré
';
- return;
- }
-
- state.allVaults.forEach((vault) => {
- const settings = allSettings[vault.name] || { hideHiddenFiles: false };
-
- const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name });
-
- // Vault header
- const header = el("div", { class: "hidden-files-vault-header" }, [el("h3", {}, [document.createTextNode(vault.name)]), el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")])]);
-
- // Hide hidden files toggle
- const toggleRow = el("div", { class: "config-row" }, [
- el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]),
- el("label", { class: "config-toggle" }, [
- el("input", {
- type: "checkbox",
- id: `hide-hidden-${vault.name}`,
- "data-vault": vault.name,
- checked: settings.hideHiddenFiles ? "true" : false,
- }),
- el("span", { class: "config-toggle-slider" }),
- ]),
- el("span", { class: "config-hint" }, [document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)")]),
- ]);
-
- vaultCard.appendChild(header);
- vaultCard.appendChild(toggleRow);
-
- container.appendChild(vaultCard);
- });
-}
-
-async function saveHiddenFilesSettings() {
- const btn = document.getElementById("cfg-save-hidden-files");
- if (btn) {
- btn.disabled = true;
- btn.textContent = t("config.saving") + "...";
- }
-
- try {
- const vaultCards = document.querySelectorAll(".hidden-files-vault-card");
- const promises = [];
-
- vaultCards.forEach((card) => {
- const vaultName = card.dataset.vault;
- const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false;
-
- const settings = {
- hideHiddenFiles,
- };
-
- promises.push(
- api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(settings),
- }),
- );
- });
-
- await Promise.all(promises);
-
- // Reload vault settings to update the cache
- await loadVaultSettings();
-
- showToast(t("config.settings_saved"), "success");
-
- // Refresh the UI to apply the filter
- await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
- } catch (err) {
- console.error("Failed to save hidden files settings:", err);
- const errorMsg = err.message || t("config.error_unknown");
- showToast(t("config.error_prefix") + ": " + errorMsg, "error");
- } finally {
- if (btn) {
- btn.disabled = false;
- btn.textContent = "💾 " + t("config.save");
- }
- }
-}
-
-// ── Webhooks UI ──
-async function loadWebhooksUI() {
- const list = document.getElementById("webhooks-list");
- if (!list) return;
- try {
- const webhooks = await api("/api/webhooks");
- renderWebhooksUI(webhooks);
- } catch { list.innerHTML = 'Admin uniquement
'; }
-}
-function renderWebhooksUI(webhooks) {
- const list = document.getElementById("webhooks-list");
- if (!list) return;
- if (!webhooks.length) { list.innerHTML = 'Aucun webhook configuré.
'; return; }
- list.innerHTML = webhooks.map(w => `
-
- ${escapeHtml(w.name)}
- ${escapeHtml(w.url)}
- ${(w.events||[]).join(", ")}
- ✕
-
- `).join("");
- list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => {
- await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" });
- loadWebhooksUI();
- }));
-}
-document.addEventListener("click", function(e) {
- if (e.target.id === "webhook-add-btn") {
- const name = document.getElementById("webhook-name-input").value.trim();
- const url = document.getElementById("webhook-url-input").value.trim();
- if (!url) { showToast(t("config.url_required"), "error"); return; }
- api("/api/webhooks", { method: "POST", body: JSON.stringify({ name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] }) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; }).catch(err => showToast(err.message, "error"));
- }
-});
-
-// ── Shares UI ──
-async function loadSharesUI() {
- const list = document.getElementById("shares-list");
- if (!list) return;
- try {
- const shares = await api("/api/shares");
- renderSharesUI(shares);
- } catch { list.innerHTML = 'Chargement...
'; }
-}
-function renderSharesUI(shares) {
- const list = document.getElementById("shares-list");
- if (!list) return;
- if (!shares.length) { list.innerHTML = 'Aucun partage actif.
'; return; }
- list.innerHTML = shares.map(s => `
-
-
${escapeHtml(s.vault)}/${escapeHtml(s.path)}
-
${s.url}
-
${s.access_count} vue(s)${s.expires_at ? ' · Expire' : ''}
-
Révoquer
-
- `).join("");
- list.querySelectorAll(".share-revoke").forEach(b => b.addEventListener("click", async () => {
- await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
- loadSharesUI();
- }));
-}
-
-// ── Share Dialog (professional) ──
-export async function openShareDialog(vault, path) {
- // First check if already shared
- let existingShare = null;
- try {
- const shares = await api("/api/shares");
- existingShare = shares.find(s => s.vault === vault && s.path === path);
- } catch (e) { /* ignore */ }
-
- const div = document.createElement("div");
- div.className = "share-dialog-overlay";
-
- const renderContent = () => {
- if (existingShare) {
- const url = window.location.origin + existingShare.url;
- const expiresInfo = existingShare.expires_at
- ? `Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}
`
- : 'Sans expiration
';
- div.innerHTML = `
-
-
📤 Document partagé
-
${escapeHtml(vault)}/${escapeHtml(path)}
- ${expiresInfo}
-
${existingShare.access_count} vue(s)
-
-
- 📋 Copier le lien
- 🗑 Révoquer
- Fermer
-
-
`;
- div.querySelector(".share-copy-btn").addEventListener("click", async () => {
- try {
- await navigator.clipboard.writeText(url);
- } catch (e) {
- // Fallback for non-HTTPS contexts
- const ta = document.createElement("textarea");
- ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px";
- document.body.appendChild(ta); ta.select(); document.execCommand("copy");
- document.body.removeChild(ta);
- }
- showToast(t("config.link_copied"), "success");
- div.remove();
- });
- div.querySelector(".share-revoke-btn").addEventListener("click", async () => {
- try {
- await api(`/api/share/${existingShare.id}`, { method: "DELETE" });
- showToast(t("config.share_revoked"), "success");
- existingShare = null;
- renderContent();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- } else {
- div.innerHTML = `
-
-
📤 Partager ce document
-
${escapeHtml(vault)}/${escapeHtml(path)}
-
Ce lien sera accessible publiquement, sans authentification .
-
- Expiration :
-
- Jamais
- 1 heure
- 24 heures
- 7 jours
- 30 jours
-
-
-
- 🔗 Créer le lien
- Fermer
-
-
`;
- div.querySelector(".share-create-btn").addEventListener("click", async () => {
- try {
- const expiry = document.getElementById("share-expiry")?.value;
- const share = await api(`/api/share/${encodeURIComponent(vault)}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }),
- });
- existingShare = share;
- renderContent();
- showToast(t("config.link_created"), "success");
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- }
- div.querySelector(".share-close-btn").addEventListener("click", () => div.remove());
- div.addEventListener("click", (e) => { if (e.target === div) div.remove(); });
- };
-
- renderContent();
- document.body.appendChild(div);
-}
-
-function renderConfigFilters() {
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
- const container = document.getElementById("config-filters-list");
-
- container.innerHTML = "";
-
- filters.forEach((filter, index) => {
- const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
- el("span", {}, [document.createTextNode(filter.pattern)]),
- el(
- "button",
- {
- class: "config-filter-toggle",
- title: filter.enabled ? "Désactiver" : "Activer",
- type: "button",
- },
- [document.createTextNode(filter.enabled ? "✓" : "○")],
- ),
- el(
- "button",
- {
- class: "config-filter-remove",
- title: "Supprimer",
- type: "button",
- },
- [document.createTextNode("×")],
- ),
- ]);
-
- const toggleBtn = badge.querySelector(".config-filter-toggle");
- const removeBtn = badge.querySelector(".config-filter-remove");
-
- toggleBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- toggleConfigFilter(index);
- });
-
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeConfigFilter(index);
- });
-
- container.appendChild(badge);
- });
-}
-
-function toggleConfigFilter(index) {
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
- if (filters[index]) {
- filters[index].enabled = !filters[index].enabled;
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
- }
-}
-
-function removeConfigFilter(index) {
- const config = TagFilterService.getConfig();
- let filters = config.tagFilters || TagFilterService.defaultFilters;
- filters = filters.filter((_, i) => i !== index);
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
-}
-
-function addConfigFilter() {
- const input = document.getElementById("config-pattern-input");
- const pattern = input.value.trim();
-
- if (!pattern) return;
-
- const regex = TagFilterService.patternToRegex(pattern);
- const config = TagFilterService.getConfig();
- const filters = config.tagFilters || TagFilterService.defaultFilters;
-
- const newFilter = { pattern, regex, enabled: true };
- filters.push(newFilter);
- config.tagFilters = filters;
- TagFilterService.saveConfig(config);
-
- input.value = "";
- renderConfigFilters();
- refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
- updateRegexPreview();
-}
-
-function updateRegexPreview() {
- const input = document.getElementById("config-pattern-input");
- const preview = document.getElementById("config-regex-preview");
- const code = document.getElementById("config-regex-code");
- const pattern = input.value.trim();
-
- if (pattern) {
- const regex = TagFilterService.patternToRegex(pattern);
- code.textContent = `^${regex}$`;
- preview.style.display = "block";
- } else {
- preview.style.display = "none";
- }
-}
-
-
-// ── AI Keys management ──
-async function loadAIKeys() {
- try {
- const data = await api("/api/config/ai-keys");
- ["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY"].forEach(k => {
- const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key";
- const input = document.getElementById(id);
- if (input && data[k]) input.placeholder = data[k];
- });
- } catch(e) {}
-}
-
-async function saveAIKeys() {
- const keys = {};
- const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY" };
- for (const [id, name] of Object.entries(map)) {
- const input = document.getElementById(id);
- if (input && input.value.trim()) keys[name] = input.value.trim();
- }
- if (!Object.keys(keys).length) { showToast(t("config.no_keys"), "warning"); return; }
- try {
- await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) });
- showToast(t("config.api_keys_saved"), "success");
- // Clear inputs
- Object.keys(map).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
- loadAIKeys();
- } catch(e) { showToast("Erreur: " + e.message, "error"); }
-}
-
-async function testAIKeys() {
- const status = document.getElementById("cfg-ai-status");
- if (status) status.textContent = "Test en cours...";
- try {
- // Test connectivity
- const results = await api("/api/config/ai-keys/test", { method: "POST" });
- var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | ");
- if (status) status.textContent = msg;
- // Fetch models for configured providers
- for (const p of ["deepseek","openrouter","gemini"]) {
- if (results[p] === "ok") {
- try {
- const m = await api("/api/config/ai-models?provider=" + p);
- var sel = document.getElementById("cfg-" + p + "-model");
- if (sel && m.models && m.models.length) {
- sel.innerHTML = '-- Modele (' + m.models.length + ') -- ';
- m.models.forEach(function(md) {
- sel.innerHTML += '' + md + ' ';
- });
- }
- } catch(e) { console.warn("Failed to fetch models for", p, e); }
- }
- }
- } catch(e) {
- if (status) status.textContent = "Erreur: " + e.message;
- }
-}
-
-
-export {
- initSidebarTabs,
- initConfigModal,
- initConfigModal as initConfigPanel,
- initHelpModal,
- closeHelpModal,
- initRecentTab,
- initEditorPocBtn,
- loadAbout as initAboutSection,
- loadHiddenFilesSettings as initHiddenFilesConfig,
- initThemePicker,
- initAboutModal,
- initProfile,
- switchSidebarTab,
-};
-
-// ── Theme Picker ─────────────────────────────────────────────────────
-function initThemePicker() {
- import('./themes.js').then(function(mod) {
- var THEMES = mod.THEMES;
- var applyTheme = mod.applyTheme;
- var getCurrentTheme = mod.getCurrentTheme;
- var getCurrentMode = mod.getCurrentMode;
-
- var grid = document.getElementById('theme-grid');
- if (!grid) return;
-
- var themeKeys = Object.keys(THEMES);
-
- function renderCards() {
- var current = getCurrentTheme();
- var mode = getCurrentMode();
- grid.innerHTML = '';
- themeKeys.forEach(function(key) {
- var t = THEMES[key];
- var v = t.modes[mode];
- if (!v) return;
- var card = document.createElement('div');
- card.className = 'theme-card' + (key === current ? ' active' : '');
- card.innerHTML =
- '' +
- '' + t.name + '
' +
- '' + t.desc + '
';
- card.addEventListener('click', function() {
- applyTheme(key, mode);
- renderCards();
- });
- grid.appendChild(card);
- });
- // Mode toggle
- var toggle = document.createElement('div');
- toggle.className = 'theme-mode-toggle';
- var dBtn = elBtn(t('theme.dark'), 'dark', mode);
- var lBtn = elBtn(t('theme.light'), 'light', mode);
- toggle.appendChild(dBtn);
- toggle.appendChild(lBtn);
- grid.appendChild(toggle);
- }
-
- function elBtn(label, m, currentMode) {
- var btn = document.createElement('button');
- btn.className = 'theme-mode-btn' + (m === currentMode ? ' active' : '');
- btn.textContent = label;
- btn.addEventListener('click', function() {
- applyTheme(getCurrentTheme(), m);
- renderCards();
- });
- return btn;
- }
-
- renderCards();
- });
-}
-
-// ── About Modal ──────────────────────────────────────────────────────
-function initAboutModal() {
- var overlay = document.getElementById('about-overlay');
- var closeBtn = document.getElementById('about-close');
- if (!overlay) return;
-
- // Open button
- var openBtn = document.getElementById('about-open-btn');
- if (openBtn) {
- openBtn.addEventListener('click', function() {
- overlay.classList.add('active');
- populateAbout();
- });
- }
-
- // Close
- closeBtn.addEventListener('click', function() { overlay.classList.remove('active'); });
- overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.classList.remove('active'); });
- document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.classList.contains('active')) overlay.classList.remove('active'); });
-
- function populateAbout() {
- // Live stats from health endpoint
- fetch('/api/health')
- .then(function(r) { return r.json(); })
- .then(function(d) {
- var fEl = document.getElementById('about-stat-files');
- var vEl = document.getElementById('about-stat-vaults');
- if (fEl) fEl.textContent = d.total_files || '—';
- if (vEl) vEl.textContent = d.vaults || '—';
- })
- .catch(function() {});
-
- // Build date (today)
- var now = new Date();
- var months = [t('month.january'),t('month.february'),t('month.march'),t('month.april'),t('month.may'),t('month.june'),t('month.july'),t('month.august'),t('month.september'),t('month.october'),t('month.november'),t('month.december')];
- var dateEl = document.getElementById('about-build-date');
- if (dateEl) dateEl.textContent = months[now.getMonth()] + ' ' + now.getFullYear();
-
- // Random joke
- var jokes = [
- t('about.joke_0'),
- t('about.joke_1'),
- t('about.joke_2'),
- t('about.joke_3'),
- t('about.joke_4'),
- t('about.joke_5'),
- t('about.joke_6'),
- t('about.joke_7'),
- t('about.joke_8'),
- t('about.joke_9')
- ];
- var jokeEl = document.getElementById('about-joke');
- if (jokeEl) jokeEl.textContent = jokes[Math.floor(Math.random() * jokes.length)];
- }
-}
-
-// ── Profile ──────────────────────────────────────────────────────────
-function initProfile() {
- var langSel = document.getElementById('profile-lang');
- var nameInp = document.getElementById('profile-name');
- var saveBtn = document.getElementById('profile-save');
- var logoutBtn = document.getElementById('profile-logout');
- var savedEl = document.getElementById('profile-saved');
- if (!langSel || !saveBtn) return;
-
- // Load saved profile
- try {
- var lang = getLocale();
- var name = localStorage.getItem('obsigate-name') || '';
- if (langSel) langSel.value = lang;
- if (nameInp) nameInp.value = name;
- } catch(e) {}
-
- saveBtn.addEventListener('click', async function() {
- try {
- var newLang = langSel.value;
- await setLocale(newLang); // Live-reload all translations
- if (nameInp) localStorage.setItem('obsigate-name', nameInp.value.trim());
-
- // Persist language to server (survives clear site data)
- try {
- await fetch('/api/auth/me', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ language: newLang }),
- });
- } catch (e) { /* non-bloquant */ }
-
- if (savedEl) { savedEl.style.display = 'inline'; setTimeout(function() { savedEl.style.display = 'none'; }, 2000); }
- } catch(e) {}
- });
-
- // Logout button — handled inline in index.html (onclick)
-}
diff --git a/desktop/frontend-static/js/dashboard.js b/desktop/frontend-static/js/dashboard.js
deleted file mode 100644
index 697a908..0000000
--- a/desktop/frontend-static/js/dashboard.js
+++ /dev/null
@@ -1,484 +0,0 @@
-// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
-import { api } from './auth.js';
-import { state } from './state.js';
-import { escapeHtml, safeCreateIcons, getFileIcon } from './utils.js';
-import { icon, openFile } from './viewer.js';
-import { TabManager, showToast } from './ui.js';
-import { t, getLocale } from './i18n.js';
-
-// ---------------------------------------------------------------------------
-// Recent files
-// ---------------------------------------------------------------------------
-let _recentRefreshTimer = null;
-let _recentTimestampTimer = null;
-let _recentFilesCache = [];
-
-// ---------------------------------------------------------------------------
-// Dashboard Recent Files Widget
-// ---------------------------------------------------------------------------
-// ── Dashboard Stats Widget ──
-const DashboardStatsWidget = {
- async load() {
- const grid = document.getElementById("dashboard-stats-grid");
- if (!grid) return;
- grid.innerHTML = `${t('common.loading')}
`;
- try {
- const data = await api("/api/dashboard");
- this.render(data);
- } catch (err) {
- grid.innerHTML = `${t('dashboard.error_prefix')}${escapeHtml(err.message)}
`;
- }
- },
- render(data) {
- const grid = document.getElementById("dashboard-stats-grid");
- if (!grid) return;
- const fmtSize = (bytes) => bytes < 1024 ? `${bytes}${t('dashboard.size_bytes')}` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)}${t('dashboard.size_kb')}` : bytes < 1073741824 ? `${(bytes/1048576).toFixed(1)}${t('dashboard.size_mb')}` : `${(bytes/1073741824).toFixed(1)}${t('dashboard.size_gb')}`;
- const items = [
- { icon: "files", label: t('dashboard.stats_files'), value: data.total_files.toLocaleString() },
- { icon: "tags", label: t('dashboard.stats_tags'), value: data.total_tags.toLocaleString() },
- { icon: "hard-drive", label: t('dashboard.stats_size'), value: fmtSize(data.total_size_bytes) },
- { icon: "folder-open", label: t('dashboard.stats_vaults'), value: data.vaults.length.toString() },
- ];
- grid.innerHTML = items.map(i => `
-
-
- ${i.value}
- ${i.label}
-
- `).join("");
- safeCreateIcons();
- }
-};
-
-// ── Dashboard Shared Widget ──
-const DashboardSharedWidget = {
- async load() {
- const grid = document.getElementById("dashboard-shared-grid");
- const empty = document.getElementById("dashboard-shared-empty");
- if (!grid) return;
- try {
- const shares = await api("/api/shares");
- if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
- if (empty) empty.style.display = "none";
- grid.innerHTML = shares.map(s => `
-
-
-
- ${s.access_count || 0} ${t('dashboard.views', {count: s.access_count || 0})}
- ${s.expires_at ? `${t('dashboard.expires', {date: new Date(s.expires_at).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US')})} ` : ""}
-
-
- ${t('dashboard.copy_btn')}
- ${t('dashboard.open_btn')}
- 🗑
-
-
- `).join("");
- lucide.createIcons();
- grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- const url = b.dataset.url;
- try { await navigator.clipboard.writeText(url); } catch { const ta = document.createElement("textarea"); ta.value=url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
- showToast(t("config.link_copied"), "success");
- }));
- grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
- e.stopPropagation();
- const card = b.closest(".shared-card");
- if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
- }));
- grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
- showToast(t("config.share_revoked"), "success");
- this.load();
- }));
- grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
- TabManager.openPreview(card.dataset.vault, card.dataset.path);
- }));
- } catch (err) { if (empty) empty.style.display = ""; }
- }
-};
-
-// ── Dashboard Conflicts Widget ──
-const DashboardConflictsWidget = {
- async load() {
- const container = document.getElementById("dashboard-conflicts-container");
- if (!container) return;
- try {
- const data = await api("/api/conflicts");
- if (data.total === 0) { container.innerHTML = ""; return; }
- this.render(data.conflicts, container);
- } catch (err) { container.innerHTML = ""; }
- },
- render(conflicts, container) {
- container.innerHTML = `
-
-
-
- ${conflicts.map(c => `
-
-
- ${escapeHtml(c.vault)}
- ${escapeHtml(c.conflict_path.split("/").pop())}
- ${t('dashboard.conflict_date')}${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}
-
-
- ${t('dashboard.keep_original')}
- ${t('dashboard.keep_conflict')}
-
-
- `).join("")}
-
-
`;
- lucide.createIcons();
- container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
- container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
- },
- async _resolve(d, action) {
- try {
- await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
- showToast(t("dashboard.conflict_resolved"), "success");
- this.load();
- } catch (err) { showToast(t('dashboard.error_prefix') + err.message, "error"); }
- }
-};
-
-const DashboardRecentWidget = {
- _cache: [],
- _currentFilter: "",
-
- async load(vaultFilter = "") {
- const v = vaultFilter || state.selectedContextVault || "all";
- this._currentFilter = v;
- this.showLoading();
-
- let url = "/api/recent?mode=opened";
- if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
-
- try {
- const data = await api(url);
- this._cache = data.files || [];
- this.render();
- } catch (err) {
- console.error("Dashboard: Failed to load recent files:", err);
- this.showError();
- }
- },
-
- async toggleBookmark(vault, path, title, card) {
- try {
- const data = await api("/api/bookmarks/toggle", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ vault, path, title }),
- });
-
- // Refresh both widgets to keep sync
- DashboardBookmarkWidget.load();
-
- // Update current card icon if it exists
- if (card) {
- const btn = card.querySelector(".dashboard-card-bookmark-btn");
- if (btn) {
- btn.classList.toggle("active", data.bookmarked);
- const icon = btn.querySelector("i");
- if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
- safeCreateIcons();
- }
- }
-
- // Check if we need to refresh the current list to reflect bookmark status across all cards
- // To avoid flickering, just update the cache and re-render if needed or do a silent refresh
- this._cache.forEach(f => {
- if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
- });
- } catch (err) {
- console.error("Failed to toggle bookmark:", err);
- showToast(t("dashboard.pin_error"), "error");
- }
- },
-
- showLoading() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (grid) grid.innerHTML = "";
- if (loading) loading.classList.add("active");
- if (empty) empty.classList.add("hidden");
- if (count) count.textContent = "";
- },
-
- render() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (loading) loading.classList.remove("active");
-
- if (!this._cache || this._cache.length === 0) {
- this.showEmpty();
- return;
- }
-
- if (empty) empty.classList.add("hidden");
- if (count) count.textContent = `${this._cache.length} ${this._cache.length > 1 ? t('dashboard.count_files_plural') : t('dashboard.count_files')}`;
-
- if (!grid) return;
- grid.innerHTML = "";
-
- this._cache.forEach((f, index) => {
- const card = this._createCard(f, index);
- grid.appendChild(card);
- });
-
- safeCreateIcons();
- },
-
- _createCard(file, index) {
- const card = document.createElement("div");
- card.className = "dashboard-card";
- card.setAttribute("data-vault", file.vault);
- card.setAttribute("data-path", file.path);
- card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
-
- // Header with icon and vault badge
- const header = document.createElement("div");
- header.className = "dashboard-card-header";
-
- const iconContainer = document.createElement("div");
- iconContainer.className = "dashboard-card-icon";
- const fileIconName = getFileIcon(file.path);
- try {
- iconContainer.appendChild(icon(fileIconName, 24));
- } catch (e) {
- console.error("Error creating icon:", fileIconName, e);
- // Fallback to default file icon
- iconContainer.appendChild(icon("file", 24));
- }
-
- const badge = document.createElement("span");
- badge.className = "dashboard-vault-badge";
- badge.textContent = file.vault;
-
- const bookmarkBtn = document.createElement("button");
- bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
- bookmarkBtn.title = file.bookmarked ? t('dashboard.bookmark_remove') : t('dashboard.bookmark_add');
- bookmarkBtn.innerHTML = ` `;
-
- bookmarkBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- this.toggleBookmark(file.vault, file.path, file.title, card);
- });
-
- header.appendChild(iconContainer);
- header.appendChild(badge);
- header.appendChild(bookmarkBtn);
- card.appendChild(header);
-
- // Title
- const title = document.createElement("h3");
- title.className = "dashboard-card-title";
- title.textContent = file.title || file.path.split("/").pop();
- title.title = file.title || file.path;
- card.appendChild(title);
-
- // Path (compact)
- const pathParts = file.path.split("/");
- if (pathParts.length > 1) {
- const path = document.createElement("div");
- path.className = "dashboard-card-path";
- path.textContent = pathParts.slice(0, -1).join(" / ");
- path.title = file.path;
- card.appendChild(path);
- }
-
- // Footer with time and tags
- const footer = document.createElement("div");
- footer.className = "dashboard-card-footer";
-
- const time = document.createElement("span");
- time.className = "dashboard-card-time";
- time.innerHTML = ` ${file.mtime_human || this._humanizeDelta(file.mtime)}`;
-
- footer.appendChild(time);
-
- // Tags
- if (file.tags && file.tags.length > 0) {
- const tags = document.createElement("div");
- tags.className = "dashboard-card-tags";
- file.tags.slice(0, 3).forEach((tag) => {
- const tagEl = document.createElement("span");
- tagEl.className = "tag-pill";
- tagEl.textContent = tag;
- tags.appendChild(tagEl);
- });
- footer.appendChild(tags);
- }
-
- card.appendChild(footer);
-
- // Click handler
- card.addEventListener("click", () => {
- openFile(file.vault, file.path);
- });
-
- return card;
- },
-
- showEmpty() {
- const grid = document.getElementById("dashboard-recent-grid");
- const loading = document.getElementById("dashboard-loading");
- const empty = document.getElementById("dashboard-recent-empty");
- const count = document.getElementById("dashboard-count");
-
- if (grid) grid.innerHTML = "";
- if (loading) loading.classList.remove("active");
- if (empty) empty.classList.remove("hidden");
- if (count) count.textContent = t('dashboard.zero_files');
- safeCreateIcons();
- },
-
- showError() {
- this.showEmpty();
- const empty = document.getElementById("dashboard-recent-empty");
- if (empty) {
- const msg = empty.querySelector("span");
- if (msg) msg.textContent = t('dashboard.load_error');
- }
- },
-
- _humanizeDelta(mtime) {
- const delta = Date.now() / 1000 - mtime;
- if (delta < 60) return t('dashboard.just_now');
- if (delta < 3600) return t('dashboard.min_ago', { n: Math.floor(delta / 60) });
- if (delta < 86400) return t('dashboard.hour_ago', { n: Math.floor(delta / 3600) });
- if (delta < 604800) return t('dashboard.day_ago', { n: Math.floor(delta / 86400) });
- return new Date(mtime * 1000).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US', { day: "numeric", month: "short", year: "numeric" });
- },
-
- populateVaultFilter() {
- const select = document.getElementById("dashboard-vault-filter");
- if (!select) return;
-
- // Keep first option "Tous les vaults"
- while (select.options.length > 1) select.remove(1);
-
- if (typeof state.allVaults !== "undefined" && Array.isArray(state.allVaults)) {
- state.allVaults.forEach((v) => {
- const opt = document.createElement("option");
- opt.value = v.name;
- opt.textContent = v.name;
- select.appendChild(opt);
- });
- }
- syncVaultSelectors();
- },
-
- init() {
- const select = document.getElementById("dashboard-vault-filter");
- if (select) {
- select.addEventListener("change", async () => {
- await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
- });
- }
-
- this.populateVaultFilter();
-
- // Quick Help toggle (with localStorage persistence)
- var qhToggle = document.getElementById('qh-toggle');
- var qhBody = document.getElementById('qh-body');
- if (qhToggle && qhBody) {
- // Restore saved state
- if (localStorage.getItem('obsigate-qh-collapsed') === '1') {
- qhBody.classList.add('collapsed');
- qhToggle.textContent = '+';
- qhToggle.title = t('dashboard.show');
- }
- qhToggle.addEventListener('click', function() {
- var collapsed = qhBody.classList.toggle('collapsed');
- qhToggle.textContent = collapsed ? '+' : '\u2212';
- qhToggle.title = collapsed ? t('dashboard.show') : t('dashboard.hide');
- localStorage.setItem('obsigate-qh-collapsed', collapsed ? '1' : '0');
- });
- }
- },
-};
-
-// ── Dashboard Bookmarks Widget ──
-// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
-const DashboardBookmarkWidget = {
- _cache: [],
- _currentFilter: "",
-
- async load(vaultFilter = "") {
- const v = vaultFilter || state.selectedContextVault || "all";
- this._currentFilter = v;
- this.showLoading();
-
- let url = "/api/bookmarks";
- if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
-
- try {
- const data = await api(url);
- this._cache = data.files || [];
- this.render();
- } catch (err) {
- console.error("Dashboard: Failed to load bookmarks:", err);
- this.showEmpty();
- }
- },
-
- showLoading() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- const section = document.getElementById("dashboard-bookmarks-section");
-
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.add("hidden");
- },
-
- render() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- const section = document.getElementById("dashboard-bookmarks-section");
-
- if (!this._cache || this._cache.length === 0) {
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.remove("hidden");
- return;
- }
-
- if (empty) empty.classList.add("hidden");
- if (!grid) return;
- grid.innerHTML = "";
-
- this._cache.forEach((f, idx) => {
- const card = DashboardRecentWidget._createCard(f, idx);
- grid.appendChild(card);
- });
-
- safeCreateIcons();
- },
-
- showEmpty() {
- const grid = document.getElementById("dashboard-bookmarks-grid");
- const empty = document.getElementById("dashboard-bookmarks-empty");
- if (grid) grid.innerHTML = "";
- if (empty) empty.classList.remove("hidden");
- }
-};
-
-export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget, DashboardSharedWidget, DashboardConflictsWidget };
diff --git a/desktop/frontend-static/js/dragdrop.js b/desktop/frontend-static/js/dragdrop.js
deleted file mode 100644
index 2c56e27..0000000
--- a/desktop/frontend-static/js/dragdrop.js
+++ /dev/null
@@ -1,329 +0,0 @@
-/**
- * Drag & Drop module for ObsiGate sidebar tree.
- *
- * Allows drag-and-drop of tree items (files and directories) onto
- * target directories or vault roots to reorganize the vault.
- *
- * Usage: import { initDragDrop } from './dragdrop.js';
- * initDragDrop() after the vault tree is ready.
- */
-
-import { api } from './auth.js';
-import { showToast } from './ui.js';
-import { refreshSidebarTreePreservingState } from './sidebar.js';
-
-// ---------------------------------------------------------------------------
-// State
-// ---------------------------------------------------------------------------
-
-/** Currently dragged item: { vault, path, type, element } */
-let dragState = null;
-
-// ---------------------------------------------------------------------------
-// CSS classes applied during drag operations
-// ---------------------------------------------------------------------------
-
-const DRAG_OVER_CLASS = 'drag-over';
-const DRAG_SOURCE_CLASS = 'drag-source';
-const DRAG_VALID_TARGET_CLASS = 'drag-valid-target';
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-/**
- * Walk up the DOM to find the closest ancestor matching the selector.
- */
-function closest(selector, el) {
- while (el && el !== document) {
- if (el.matches && el.matches(selector)) return el;
- el = el.parentNode;
- }
- return null;
-}
-
-/**
- * Determine if an element is a valid drop target (directory or vault root).
- * Returns { vault, targetDir } or null.
- */
-function getDropTarget(el) {
- // Directory tree item
- const dirItem = closest('.tree-item[data-path]', el);
- if (dirItem) {
- // Only directories are valid targets
- const icon = dirItem.querySelector('[data-lucide]');
- const isDir = icon && (icon.getAttribute('data-lucide') === 'folder'
- || icon.getAttribute('data-lucide') === 'chevron-right'
- || icon.getAttribute('data-lucide') === 'chevron-down');
- // Heuristic: if the tree item has a child container sibling, it's a directory
- const hasChildren = dirItem.nextElementSibling
- && dirItem.nextElementSibling.classList.contains('tree-children');
- if (isDir || hasChildren) {
- return {
- vault: dirItem.dataset.vault,
- targetDir: dirItem.dataset.path,
- };
- }
- return null;
- }
-
- // Vault item (root of a vault)
- const vaultItem = closest('.tree-item.vault-item[data-vault]', el);
- if (vaultItem) {
- return {
- vault: vaultItem.dataset.vault,
- targetDir: '', // empty = vault root
- };
- }
-
- return null;
-}
-
-/**
- * Highlight drop target and its parent chain.
- */
-function highlightDropTarget(el) {
- const target = getDropTarget(el);
- if (!target) return;
-
- if (target.targetDir) {
- // Highlight the directory
- const dirEl = document.querySelector(
- `.tree-item[data-vault="${CSS.escape(target.vault)}"][data-path="${CSS.escape(target.targetDir)}"]`
- );
- if (dirEl) dirEl.classList.add(DRAG_OVER_CLASS);
- } else {
- // Highlight the vault
- const vaultEl = document.querySelector(`.tree-item.vault-item[data-vault="${CSS.escape(target.vault)}"]`);
- if (vaultEl) vaultEl.classList.add(DRAG_OVER_CLASS);
- }
-}
-
-/**
- * Remove all drop target highlights.
- */
-function clearAllHighlights() {
- document.querySelectorAll(`.${DRAG_OVER_CLASS}`).forEach(el => el.classList.remove(DRAG_OVER_CLASS));
-}
-
-/**
- * Check if the source item is being dropped on itself or inside itself.
- */
-function isInvalidDrop(source, target) {
- if (source.vault !== target.vault) return true; // cross-vault not supported
- if (!target.targetDir) {
- // Target is vault root — check if source is already at root
- const sourceParent = source.path.includes('/')
- ? source.path.substring(0, source.path.lastIndexOf('/'))
- : '';
- return sourceParent === '';
- }
- // Can't drop a directory into itself or its children
- if (source.type === 'directory') {
- if (source.path === target.targetDir) return true; // dropping on itself
- if (target.targetDir.startsWith(source.path + '/')) return true; // dropping into a child
- }
- // Check if source is already in this directory
- const sourceParent = source.path.includes('/')
- ? source.path.substring(0, source.path.lastIndexOf('/'))
- : '';
- if (sourceParent === target.targetDir) return true; // same directory
- return false;
-}
-
-// ---------------------------------------------------------------------------
-// Event handlers
-// ---------------------------------------------------------------------------
-
-function onDragStart(e) {
- const treeItem = closest('.tree-item', e.target);
- if (!treeItem) return;
-
- // Don't allow dragging vault items themselves
- if (treeItem.classList.contains('vault-item')) return;
-
- const vault = treeItem.dataset.vault;
- const path = treeItem.dataset.path;
- if (!vault || path === undefined) return;
-
- // Determine type: directory or file
- const hasChildren = treeItem.nextElementSibling
- && treeItem.nextElementSibling.classList.contains('tree-children');
- const type = hasChildren ? 'directory' : 'file';
-
- dragState = { vault, path, type, element: treeItem };
-
- // Set drag data and visual feedback
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/plain', path);
-
- // Visual: reduce opacity of source
- treeItem.classList.add(DRAG_SOURCE_CLASS);
-
- // Add valid-target indicator to all directories and vaults
- setTimeout(() => {
- document.querySelectorAll('.tree-item').forEach(item => {
- if (item === treeItem) return;
- if (item.classList.contains('vault-item')) {
- item.classList.add(DRAG_VALID_TARGET_CLASS);
- } else {
- const icon = item.querySelector('[data-lucide]');
- const hasChildContainer = item.nextElementSibling
- && item.nextElementSibling.classList.contains('tree-children');
- if (icon && (icon.getAttribute('data-lucide') === 'folder' || hasChildContainer)) {
- item.classList.add(DRAG_VALID_TARGET_CLASS);
- }
- }
- });
- }, 0);
-}
-
-function onDragOver(e) {
- if (!dragState) return;
- e.preventDefault(); // Required to allow drop
- e.dataTransfer.dropEffect = 'move';
-
- clearAllHighlights();
-
- const target = getDropTarget(e.target);
- if (!target || isInvalidDrop(dragState, target)) {
- e.dataTransfer.dropEffect = 'none';
- return;
- }
-
- highlightDropTarget(e.target);
-}
-
-function onDragLeave(e) {
- // Only clear when actually leaving the container, not entering a child
- const container = document.getElementById('vault-tree');
- if (!container) return;
- // Use a small delay to avoid flickering when moving between children
- clearAllHighlights();
-}
-
-function onDragEnd() {
- clearAllHighlights();
-
- // Remove drag source visual
- if (dragState && dragState.element) {
- dragState.element.classList.remove(DRAG_SOURCE_CLASS);
- }
-
- // Remove valid-target indicators
- document.querySelectorAll(`.${DRAG_VALID_TARGET_CLASS}`).forEach(el => {
- el.classList.remove(DRAG_VALID_TARGET_CLASS);
- });
-
- dragState = null;
-}
-
-async function onDrop(e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (!dragState) return;
-
- const target = getDropTarget(e.target);
- if (!target || isInvalidDrop(dragState, target)) {
- onDragEnd();
- return;
- }
-
- const { vault, path, type } = dragState;
- const { vault: targetVault, targetDir } = target;
-
- // Don't move if source and target are the same vault
- if (vault !== targetVault) {
- showToast(t('dragdrop.cross_vault_unsupported'), 'error');
- onDragEnd();
- return;
- }
-
- // Save drag state before cleanup
- const savedState = { ...dragState };
-
- // Clean up visual state immediately
- onDragEnd();
-
- // Call API
- try {
- const response = await api(`/api/move/${encodeURIComponent(vault)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- source_path: path,
- destination_dir: targetDir,
- }),
- });
-
- if (response && response.success) {
- showToast(`Moved ${type} to ${targetDir || 'vault root'}`, 'success');
- // Refresh the tree preserving expanded state
- await refreshSidebarTreePreservingState();
- } else {
- showToast(t('dragdrop.move_failed'), 'error');
- }
- } catch (err) {
- console.error('Drag & drop move failed:', err);
- const detail = err?.detail || err?.message || 'Unknown error';
- showToast(`Move failed: ${detail}`, 'error');
- // Refresh anyway in case the filesystem state changed
- try { await refreshSidebarTreePreservingState(); } catch (_) { /* ignore */ }
- }
-}
-
-// ---------------------------------------------------------------------------
-// Initialization
-// ---------------------------------------------------------------------------
-
-/**
- * Attach drag-and-drop event listeners to the vault tree container.
- * Uses event delegation on the #vault-tree element.
- */
-export function initDragDrop() {
- const container = document.getElementById('vault-tree');
- if (!container) {
- console.warn('DragDrop: vault-tree container not found');
- return;
- }
-
- // Remove previous listeners if any (idempotent)
- container.removeEventListener('dragstart', onDragStart);
- container.removeEventListener('dragover', onDragOver);
- container.removeEventListener('dragleave', onDragLeave);
- container.removeEventListener('dragend', onDragEnd);
- container.removeEventListener('drop', onDrop);
-
- // Attach listeners with delegation
- container.addEventListener('dragstart', onDragStart);
- container.addEventListener('dragover', onDragOver);
- container.addEventListener('dragleave', onDragLeave);
- container.addEventListener('dragend', onDragEnd);
- container.addEventListener('drop', onDrop);
-
- // Make all existing tree items draggable
- makeAllDraggable();
-
- // Re-apply draggable after any DOM mutation in the tree
- const observer = new MutationObserver(() => {
- makeAllDraggable();
- });
- observer.observe(container, { childList: true, subtree: true });
-}
-
-/**
- * Make all tree items (files and directories) draggable.
- * Vault items themselves are NOT draggable.
- */
-function makeAllDraggable() {
- const container = document.getElementById('vault-tree');
- if (!container) return;
-
- container.querySelectorAll('.tree-item').forEach(item => {
- if (item.classList.contains('vault-item')) return;
- if (!item.hasAttribute('draggable')) {
- item.setAttribute('draggable', 'true');
- }
- });
-}
diff --git a/desktop/frontend-static/js/graph.js b/desktop/frontend-static/js/graph.js
deleted file mode 100644
index 3ba24c0..0000000
--- a/desktop/frontend-static/js/graph.js
+++ /dev/null
@@ -1,949 +0,0 @@
-/* ObsiGate — Graph View: interactive file/folder relationship visualization.
- Phase 2: theme colors, tooltips, depth slider, full-vault, search, backlinks. */
-import { api } from './auth.js';
-import { t } from './i18n.js';
-import { safeCreateIcons } from './utils.js';
-import { openFile } from './viewer.js';
-
-// ---------------------------------------------------------------------------
-// Theme-aware color helpers
-// ---------------------------------------------------------------------------
-function _cssVar(name, fallback) {
- return getComputedStyle(document.body).getPropertyValue(name).trim() || fallback;
-}
-
-const COLORS = {
- get bg() { return _cssVar('--bg-primary', '#1e1e1e'); },
- get text() { return _cssVar('--text-primary', '#ddd'); },
- get accent() { return _cssVar('--accent-color', '#2563eb'); },
- get muted() { return _cssVar('--text-muted', '#888'); },
- get border() { return _cssVar('--border-color', '#333'); },
- dir: '#5b9bd5',
- md: '#70ad47',
- other: '#999',
- vault: '#ffc000',
- backlink: '#e74c3c',
- highlight: '#ff6b6b',
-};
-
-// ---------------------------------------------------------------------------
-// GraphViewManager
-// ---------------------------------------------------------------------------
-export const GraphViewManager = {
- _canvas: null,
- _ctx: null,
- _nodes: [],
- _edges: [],
- _offsetX: 0,
- _offsetY: 0,
- _zoom: 1,
- _dragging: false,
- _dragNode: null,
- _panning: false,
- _panStartX: 0,
- _panStartY: 0,
- _animFrame: null,
- _vault: null,
- _path: null,
- _nodePositions: {},
- _width: 0,
- _height: 0,
- _scope: 'directory',
- _depth: 1,
- _searchTerm: '',
- _tagFilter: '',
- _hoveredNode: null,
- _infoPanel: null,
- _infoContent: null,
- _previewPanel: null,
- _previewContent: null,
- _previewCache: {},
- _previewPending: null,
- _ctrlHoverNode: null,
- _navHistory: [],
- _navIndex: -1,
-
- async open(vault, path, type) {
- // Push current state to history before navigating (unless navigating via back/fwd)
- if (this._vault && !this._navNavigating) {
- this._pushHistory(this._vault, this._path || '');
- }
- this._navNavigating = false;
-
- this._vault = vault;
- this._path = path;
-
- const modal = document.getElementById('graph-modal');
- const title = document.getElementById('graph-title');
- const statusEl = document.getElementById('graph-status');
- const canvas = document.getElementById('graph-canvas');
- const depthSlider = document.getElementById('graph-depth');
-
- if (!modal || !canvas) return;
-
- this._infoPanel = document.getElementById('graph-info-panel');
- this._infoContent = document.getElementById('graph-info-content');
- this._previewPanel = document.getElementById('graph-preview-panel');
- this._previewContent = document.getElementById('graph-preview-content');
- this._depth = depthSlider ? parseInt(depthSlider.value) : 1;
- this._scope = 'directory';
-
- title.textContent = t('graph.title', { vault, path: path ? '/' + path : '' });
- statusEl.textContent = t('graph.loading');
- modal.classList.add('active');
-
- this._canvas = canvas;
- this._ctx = canvas.getContext('2d');
- this._resetView();
-
- await this._fetchAndRender();
- this._updateNavButtons();
- safeCreateIcons();
- },
-
- _cache: {},
- _cacheKey: '',
-
- _getCacheKey() {
- return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}|${this._tagFilter}`;
- },
-
- async _fetchAndRender() {
- const statusEl = document.getElementById('graph-status');
- if (!statusEl) return;
-
- const cacheKey = this._getCacheKey();
-
- // Use cache if same parameters
- if (this._cache[cacheKey]) {
- const cached = this._cache[cacheKey];
- this._nodes = cached.nodes;
- this._edges = cached.edges;
- this._scope = cached.scope;
- const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
- statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })} ${t('graph.cache_label')}`;
- this._initLayout();
- this._startRender();
- return;
- }
-
- const params = new URLSearchParams();
- if (this._path) params.set('path', this._path);
- params.set('depth', String(this._depth));
- params.set('scope', this._scope);
- if (this._tagFilter) params.set('tag', this._tagFilter);
-
- try {
- const data = await api(
- `/api/graph/${encodeURIComponent(this._vault)}?${params.toString()}`
- );
- this._nodes = data.nodes || [];
- this._edges = data.edges || [];
- this._scope = data.scope || 'directory';
-
- // Cache the result
- this._cache[cacheKey] = {
- nodes: this._nodes,
- edges: this._edges,
- scope: this._scope,
- };
- this._cacheKey = cacheKey;
-
- const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
- statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })}`;
- this._initLayout();
- this._startRender();
- } catch (err) {
- statusEl.textContent = t('graph.error_loading');
- console.error('Graph error:', err);
- }
- },
-
- reload() {
- // Called when depth slider or full-vault button changes
- this._resetView();
- this._fetchAndRender();
- },
-
- setDepth(depth) {
- this._depth = depth;
- this.reload();
- },
-
- toggleScope() {
- this._scope = this._scope === 'full' ? 'directory' : 'full';
- const btn = document.getElementById('graph-full-vault');
- if (btn) btn.textContent = this._scope === 'full' ? t('graph.btn_directory') : t('graph.btn_full');
- this.reload();
- },
-
- setSearch(term) {
- this._searchTerm = term;
- // Parse unified search: #tag → tag filter, otherwise → node search
- if (term.startsWith('#')) {
- const tag = term.substring(1).trim();
- if (tag && tag !== this._tagFilter) {
- this._tagFilter = tag;
- this.reload();
- } else if (!tag && this._tagFilter) {
- this._tagFilter = '';
- this.reload();
- }
- } else {
- if (this._tagFilter) {
- this._tagFilter = '';
- this.reload();
- return;
- }
- if (term && term.length >= 2) {
- this.reload();
- } else if (!term && this._searchTerm !== term) {
- this.reload();
- }
- }
- },
-
- close() {
- const modal = document.getElementById('graph-modal');
- if (modal) modal.classList.remove('active');
- this._navHistory = [];
- this._navIndex = -1;
- if (this._animFrame) {
- cancelAnimationFrame(this._animFrame);
- this._animFrame = null;
- }
- this._hideTooltip();
- },
-
- _resetView() {
- this._offsetX = 0;
- this._offsetY = 0;
- this._zoom = 1;
- this._nodePositions = {};
- },
-
- _pushHistory(vault, path) {
- // Truncate forward history if we navigated back then took a new path
- if (this._navIndex < this._navHistory.length - 1) {
- this._navHistory = this._navHistory.slice(0, this._navIndex + 1);
- }
- this._navHistory.push({ vault, path });
- this._navIndex = this._navHistory.length - 1;
- // Keep max 50 entries
- if (this._navHistory.length > 50) {
- this._navHistory.shift();
- this._navIndex--;
- }
- this._updateNavButtons();
- },
-
- _updateNavButtons() {
- const backBtn = document.getElementById('graph-nav-back');
- const fwdBtn = document.getElementById('graph-nav-fwd');
- const upBtn = document.getElementById('graph-nav-up');
- if (backBtn) {
- backBtn.disabled = this._navIndex <= 0;
- backBtn.style.opacity = this._navIndex > 0 ? '1' : '0.3';
- }
- if (fwdBtn) {
- fwdBtn.disabled = this._navIndex >= this._navHistory.length - 1;
- fwdBtn.style.opacity = this._navIndex < this._navHistory.length - 1 ? '1' : '0.3';
- }
- // Show "↑ Parent" if we have a path (subfolder) or history
- if (upBtn) {
- const hasParent = !!(this._path && this._path.includes('/'));
- const hasHistory = this._navIndex > 0;
- upBtn.style.display = (hasParent || hasHistory) ? '' : 'none';
- }
- },
-
- goBack() {
- if (this._navIndex <= 0) return;
- this._navIndex--;
- const entry = this._navHistory[this._navIndex];
- this._navNavigating = true;
- this.open(entry.vault, entry.path);
- },
-
- goForward() {
- if (this._navIndex >= this._navHistory.length - 1) return;
- this._navIndex++;
- const entry = this._navHistory[this._navIndex];
- this._navNavigating = true;
- this.open(entry.vault, entry.path);
- },
-
- goUp() {
- if (!this._path || !this._path.includes('/')) {
- // No subfolder — go to previous history entry
- this.goBack();
- return;
- }
- // Go to parent directory
- const parentPath = this._path.split('/').slice(0, -1).join('/');
- this.open(this._vault, parentPath);
- },
-
- _initLayout() {
- const w = this._canvas.parentElement.clientWidth;
- const h = this._canvas.parentElement.clientHeight;
- this._canvas.width = w * devicePixelRatio;
- this._canvas.height = h * devicePixelRatio;
- this._canvas.style.width = w + 'px';
- this._canvas.style.height = h + 'px';
- this._width = w;
- this._height = h;
- this._ctx.setTransform(1, 0, 0, 1, 0, 0);
- this._ctx.scale(devicePixelRatio, devicePixelRatio);
-
- const cx = w / 2;
- const cy = h / 2;
- const radius = Math.min(w, h) * 0.35;
-
- this._nodes.forEach((node, i) => {
- const angle = (2 * Math.PI * i) / Math.max(this._nodes.length, 1);
- this._nodePositions[node.id] = {
- x: cx + radius * Math.cos(angle),
- y: cy + radius * Math.sin(angle),
- vx: 0,
- vy: 0,
- };
- });
- },
-
- _startRender() {
- const self = this;
- let lastTime = 0;
-
- const loop = (time) => {
- const dt = Math.min((time - lastTime) / 1000, 0.1);
- lastTime = time;
- self._simulate(dt);
- self._draw();
- self._animFrame = requestAnimationFrame(loop);
- };
-
- this._animFrame = requestAnimationFrame(loop);
- },
-
- _simulate(dt) {
- if (this._dragging || this._dragNode) return;
-
- const positions = this._nodePositions;
- const cx = this._width / 2;
- const cy = this._height / 2;
-
- // Spring forces (edges) — unchanged
- for (const edge of this._edges) {
- const a = positions[edge.source];
- const b = positions[edge.target];
- if (!a || !b) continue;
-
- const dx = b.x - a.x;
- const dy = b.y - a.y;
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
- const targetLen = 80;
- const force = (dist - targetLen) * 0.01;
- const fx = (dx / dist) * force;
- const fy = (dy / dist) * force;
-
- a.vx += fx;
- a.vy += fy;
- b.vx -= fx;
- b.vy -= fy;
- }
-
- // Repulsion: Barnes-Hut for >200 nodes, naive O(n²) otherwise
- if (this._nodes.length > 200) {
- this._barnesHutRepulsion(positions);
- } else {
- this._naiveRepulsion(positions);
- }
-
- // Center gravity
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- p.vx += (cx - p.x) * 0.001;
- p.vy += (cy - p.y) * 0.001;
- }
-
- // Apply velocities with damping
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- p.vx *= 0.9;
- p.vy *= 0.9;
- p.x += p.vx * dt * 60;
- p.y += p.vy * dt * 60;
- }
- },
-
- _naiveRepulsion(positions) {
- for (const n1 of this._nodes) {
- for (const n2 of this._nodes) {
- if (n1.id === n2.id) continue;
- const a = positions[n1.id];
- const b = positions[n2.id];
- if (!a || !b) continue;
- const dx = b.x - a.x;
- const dy = b.y - a.y;
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
- const force = 2000 / (dist * dist);
- const fx = (dx / dist) * force;
- const fy = (dy / dist) * force;
- a.vx -= fx;
- a.vy -= fy;
- }
- }
- },
-
- // Barnes-Hut quadtree for O(n log n) repulsion
- _barnesHutRepulsion(positions) {
- // Build quadtree
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- if (p.x < minX) minX = p.x;
- if (p.y < minY) minY = p.y;
- if (p.x > maxX) maxX = p.x;
- if (p.y > maxY) maxY = p.y;
- }
- const root = this._buildQuadTree(positions, minX, minY, maxX, maxY);
-
- // Apply forces from quadtree
- const theta = 0.9; // Barnes-Hut opening angle
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (!p) continue;
- this._applyQuadTreeForce(root, p, theta);
- }
- },
-
- _buildQuadTree(positions, x0, y0, x1, y1) {
- const cx = (x0 + x1) / 2;
- const cy = (y0 + y1) / 2;
- const cell = { x0, y0, x1, y1, cx, cy, mass: 0, mx: 0, my: 0, children: null };
-
- // Collect nodes in this cell
- const contained = [];
- for (const node of this._nodes) {
- const p = positions[node.id];
- if (p && p.x >= x0 && p.x < x1 && p.y >= y0 && p.y < y1) {
- contained.push(p);
- }
- }
-
- if (contained.length === 0) return cell;
- if (contained.length === 1) {
- cell.mass = 1;
- cell.mx = contained[0].x;
- cell.my = contained[0].y;
- return cell;
- }
-
- // Subdivide
- const midX = cx;
- const midY = cy;
- cell.children = [
- this._buildQuadTree(positions, x0, y0, midX, midY), // NW
- this._buildQuadTree(positions, midX, y0, x1, midY), // NE
- this._buildQuadTree(positions, x0, midY, midX, y1), // SW
- this._buildQuadTree(positions, midX, midY, x1, y1), // SE
- ];
-
- // Compute center of mass
- let totalMass = 0;
- let sumX = 0, sumY = 0;
- for (const child of cell.children) {
- if (child.mass > 0) {
- totalMass += child.mass;
- sumX += child.mx * child.mass;
- sumY += child.my * child.mass;
- }
- }
- cell.mass = totalMass;
- cell.mx = sumX / totalMass;
- cell.my = sumY / totalMass;
-
- return cell;
- },
-
- _applyQuadTreeForce(cell, p, theta) {
- if (cell.mass === 0) return;
-
- const dx = cell.mx - p.x;
- const dy = cell.my - p.y;
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
- const size = cell.x1 - cell.x0;
-
- // If cell is far enough or is a leaf, apply force from center of mass
- if (!cell.children || size / dist < theta) {
- const force = 2000 * cell.mass / (dist * dist);
- const fx = (dx / dist) * force;
- const fy = (dy / dist) * force;
- p.vx -= fx;
- p.vy -= fy;
- return;
- }
-
- // Otherwise recurse into children
- for (const child of cell.children) {
- this._applyQuadTreeForce(child, p, theta);
- }
- },
-
- _draw() {
- const ctx = this._ctx;
- const w = this._width;
- const h = this._height;
-
- ctx.save();
- ctx.clearRect(0, 0, w, h);
-
- ctx.translate(this._offsetX, this._offsetY);
- ctx.scale(this._zoom, this._zoom);
-
- // Draw edges
- for (const edge of this._edges) {
- const a = this._nodePositions[edge.source];
- const b = this._nodePositions[edge.target];
- if (!a || !b) continue;
-
- ctx.beginPath();
- ctx.moveTo(a.x, a.y);
- ctx.lineTo(b.x, b.y);
-
- if (edge.relation === 'backlink') {
- ctx.strokeStyle = COLORS.backlink;
- ctx.lineWidth = 1.5;
- ctx.setLineDash([3, 3]);
- } else if (edge.relation === 'wikilink') {
- ctx.strokeStyle = COLORS.accent;
- ctx.lineWidth = 2;
- ctx.setLineDash([4, 4]);
- } else {
- ctx.strokeStyle = COLORS.muted;
- ctx.lineWidth = 1;
- ctx.setLineDash([]);
- }
- ctx.stroke();
- }
- ctx.setLineDash([]);
-
- // Draw nodes
- const searchLower = this._searchTerm.toLowerCase();
- for (const node of this._nodes) {
- if (!this._isNodeVisible(node)) continue;
- const p = this._nodePositions[node.id];
- if (!p) continue;
-
- const links = (node.incoming_count || 0) + (node.outgoing_count || 0);
- const r = Math.max(5, Math.min(22, 6 + Math.sqrt(Math.max(node.size || 100, links * 200)) / 100));
-
- // Highlight if search match
- const isHighlighted = searchLower && (
- node.name.toLowerCase().includes(searchLower) ||
- (node.tags || []).some(t => t.toLowerCase().includes(searchLower))
- );
-
- ctx.beginPath();
- ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
-
- switch (node.type) {
- case 'directory':
- ctx.fillStyle = COLORS.dir;
- break;
- case 'file':
- ctx.fillStyle = (node.path || '').endsWith('.md') ? COLORS.md : COLORS.other;
- break;
- case 'vault':
- ctx.fillStyle = COLORS.vault;
- break;
- default:
- ctx.fillStyle = COLORS.other;
- }
-
- if (isHighlighted) {
- ctx.shadowColor = COLORS.highlight;
- ctx.shadowBlur = 12;
- }
-
- ctx.fill();
-
- if (isHighlighted) {
- ctx.shadowBlur = 0;
- ctx.strokeStyle = COLORS.highlight;
- ctx.lineWidth = 2.5;
- } else {
- ctx.strokeStyle = COLORS.bg;
- ctx.lineWidth = 1.5;
- }
- ctx.stroke();
-
- // Node label
- const label = node.name.length > 20 ? node.name.slice(0, 18) + '...' : node.name;
- ctx.font = `${isHighlighted ? 12 : 11} / ${this._zoom}px -apple-system, sans-serif`;
- ctx.fillStyle = isHighlighted ? COLORS.highlight : COLORS.text;
- ctx.textAlign = 'center';
- ctx.fillText(label, p.x, p.y + r + 12 / this._zoom);
- }
-
- ctx.restore();
- },
-
- _getNodeAt(screenX, screenY) {
- const x = (screenX - this._offsetX) / this._zoom;
- const y = (screenY - this._offsetY) / this._zoom;
-
- for (const node of this._nodes) {
- const p = this._nodePositions[node.id];
- if (!p) continue;
- const links = (node.incoming_count || 0) + (node.outgoing_count || 0);
- const r = Math.max(5, Math.min(22, 6 + Math.sqrt(Math.max(node.size || 100, links * 200)) / 100));
- const dx = x - p.x;
- const dy = y - p.y;
- if (dx * dx + dy * dy <= r * r + 100) {
- return { node, pos: p };
- }
- }
- return null;
- },
-
- _showTooltip(node, screenX, screenY, isPreview) {
- if (isPreview) return; // Handled by _showPreviewTooltip
- if (!this._infoContent) return;
- const cacheKey = `${this._vault}:${node.path}`;
- const cached = this._previewCache[cacheKey];
- const meta = cached?.meta;
- // Build metadata summary
- let metaRows = '';
- if (meta?.frontmatter && Object.keys(meta.frontmatter).length > 0) {
- const fm = meta.frontmatter;
- const rows = [];
- if (fm.auteur) rows.push(`✍️ Auteur ${fm.auteur}
`);
- if (fm.date || fm.creation_date) rows.push(`📅 Date ${fm.date || fm.creation_date}
`);
- if (fm.statut) rows.push(`📌 Statut ${fm.statut}
`);
- if (fm.catégorie || fm.categorie) rows.push(`${t('graph.category')} ${fm.catégorie || fm.categorie}
`);
- if (fm.publish) rows.push(`✅ ${t('graph.published')}
`);
- if (fm.favoris) rows.push('⭐ ' + t('graph.favorites') + '
');
- if (rows.length > 0) metaRows = rows.join('');
- }
- const tags = (meta?.tags || node.tags || []).slice(0, 6);
- const inc = node.incoming_count || 0;
- const out = node.outgoing_count || 0;
- const typeIcon = node.type === 'directory' ? '📁' : '📄';
- const typeLabel = node.type === 'directory' ? t('graph.type_directory') : (node.path || '').endsWith('.md') ? t('graph.type_markdown') : t('graph.type_file');
-
- this._infoContent.innerHTML = `
-
-
${typeIcon}
-
-
${meta?.title || node.name}
-
${node.path}
-
-
-
- ${typeLabel}
- ${inc + out > 0 ? `🔗 ${out}→ · ${inc}← ` : ''}
-
- ${metaRows ? `${metaRows}
` : ''}
- ${tags.length > 0 ? `${tags.map(t => `#${t} `).join('')}
` : ''}
- ${node.type === 'file' ? '' + t('graph.ctrl_hover_preview') + '
' : ''}
- `;
- this._infoPanel.style.display = 'block';
- },
-
- _hideTooltip() {
- // Panels are now sticky — only hide via close button
- },
-
- _showPreviewTooltip(node, preview, screenX, screenY) {
- if (!this._previewContent || this._ctrlHoverNode !== node) return;
- const cacheKey = `${this._vault}:${node.path}`;
- const cached = this._previewCache[cacheKey];
- const meta = cached?.meta;
- // Format frontmatter summary
- let fmSummary = '';
- if (meta?.frontmatter && Object.keys(meta.frontmatter).length > 0) {
- const fm = meta.frontmatter;
- const items = [];
- if (fm.auteur) items.push(`✍️ ${fm.auteur}`);
- if (fm.date || fm.creation_date) items.push(`📅 ${fm.date || fm.creation_date}`);
- if (fm.statut) items.push(`📌 ${fm.statut}`);
- if (fm.publish) items.push(t('graph.published_badge'));
- if (fm.favoris) items.push('⭐ favoris');
- if (items.length > 0) fmSummary = `${items.join(' · ')}
`;
- }
- const tags = (meta?.tags || node.tags || []).slice(0, 8);
- this._previewContent.innerHTML = `
- ${meta?.title || node.name}
- ${node.path}
- ${fmSummary}
- ${tags.length > 0 ? `${tags.map(t => `#${t} `).join('')}
` : ''}
- ${preview}
- `;
- this._previewPanel.style.display = 'block';
- },
-
- // --- Advanced controls ---
-
- _isNodeVisible(node) {
- const showDir = document.getElementById('graph-filter-dir')?.checked ?? true;
- const showMd = document.getElementById('graph-filter-md')?.checked ?? true;
- const showOther = document.getElementById('graph-filter-other')?.checked ?? true;
- if (node.type === 'directory') return showDir;
- if (node.type === 'file' && (node.path || '').endsWith('.md')) return showMd;
- if (node.type === 'file') return showOther;
- return true;
- },
-
- applyTypeFilter() {
- // Filters are applied during _draw() — nodes/edges not in visible set are skipped
- },
-
- exportPNG() {
- const link = document.createElement('a');
- link.download = `obsigate-graph-${this._vault}-${Date.now()}.png`;
- link.href = this._canvas.toDataURL('image/png');
- link.click();
- },
-
- toggleFullscreen() {
- const container = this._canvas?.parentElement?.parentElement; // editor-container
- if (!container) return;
- if (document.fullscreenElement) {
- document.exitFullscreen();
- } else {
- container.requestFullscreen();
- }
- // Re-init layout after fullscreen change
- setTimeout(() => this._onResize(), 300);
- },
-
- focusNode(nodeId) {
- const pos = this._nodePositions[nodeId];
- if (!pos) return;
- this._offsetX = this._width / 2 - pos.x * this._zoom;
- this._offsetY = this._height / 2 - pos.y * this._zoom;
- },
-
- _onMouseDown(e) {
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- const hit = this._getNodeAt(mx, my);
- if (hit) {
- this._dragging = true;
- this._dragNode = hit;
- this._canvas.style.cursor = 'grabbing';
- } else {
- this._panning = true;
- this._panStartX = e.clientX - this._offsetX;
- this._panStartY = e.clientY - this._offsetY;
- this._canvas.style.cursor = 'grabbing';
- }
- },
-
- _onMouseMove(e) {
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- if (this._dragging && this._dragNode) {
- this._dragNode.pos.x = (mx - this._offsetX) / this._zoom;
- this._dragNode.pos.y = (my - this._offsetY) / this._zoom;
- this._dragNode.pos.vx = 0;
- this._dragNode.pos.vy = 0;
- } else if (this._panning) {
- this._offsetX = e.clientX - this._panStartX;
- this._offsetY = e.clientY - this._panStartY;
- } else {
- const hit = this._getNodeAt(mx, my);
- if (hit) {
- this._canvas.style.cursor = 'pointer';
- this._canvas.title = hit.node.type === 'file'
- ? `📄 ${hit.node.name} (${t('graph.click_to_open')}${e.ctrlKey ? ', ' + t('graph.ctrl_hover_preview_short') : ''})`
- : `📁 ${hit.node.name} (cliquer pour explorer)`;
-
- // Ctrl+hover: fetch content preview for file nodes
- if (e.ctrlKey && hit.node.type === 'file') {
- if (this._ctrlHoverNode !== hit.node) {
- this._ctrlHoverNode = hit.node;
- this._fetchPreview(hit.node, e.clientX, e.clientY);
- }
- this._showTooltip(hit.node, e.clientX, e.clientY, true);
- } else {
- this._ctrlHoverNode = null;
- this._showTooltip(hit.node, e.clientX, e.clientY, false);
- }
- } else {
- this._canvas.style.cursor = 'grab';
- this._canvas.title = '';
- this._ctrlHoverNode = null;
- this._hideTooltip();
- }
- }
- },
-
- async _fetchPreview(node, screenX, screenY) {
- const cacheKey = `${this._vault}:${node.path}`;
- if (this._previewCache[cacheKey] !== undefined) {
- const cached = this._previewCache[cacheKey];
- this._showPreviewTooltip(node, cached.preview, screenX, screenY);
- return;
- }
- // Show loading indicator
- this._showPreviewTooltip(node, '' + t('common.loading') + ' ', screenX, screenY);
- try {
- const data = await api(`/api/file/${encodeURIComponent(this._vault)}?path=${encodeURIComponent(node.path)}`);
- // Strip HTML tags, keep line breaks, limit to 600 chars
- const text = (data.html || '').replace(/<[^>]*>/g, '').replace(/\n+/g, '\n').trim();
- const preview = text.substring(0, 600) + (text.length > 600 ? '…' : '');
- // Cache both preview text and full metadata for tooltips
- this._previewCache[cacheKey] = {
- preview,
- meta: {
- title: data.title,
- frontmatter: data.frontmatter || {},
- tags: data.tags || [],
- path: node.path,
- }
- };
- this._showPreviewTooltip(node, preview, screenX, screenY);
- } catch {
- this._previewCache[cacheKey] = { preview: '(impossible de charger)', meta: null };
- this._showPreviewTooltip(node, '(impossible de charger le contenu)', screenX, screenY);
- }
- },
-
- _onMouseUp(e) {
- if (this._dragging && this._dragNode) {
- const node = this._dragNode.node;
- this._dragging = false;
- this._dragNode = null;
- this._canvas.style.cursor = 'grab';
-
- if (node.type === 'file') {
- this.close();
- openFile(this._vault, node.path);
- } else if (node.type === 'directory' || node.type === 'vault') {
- this.close();
- this._path = node.path || '';
- this.open(this._vault, this._path, node.type);
- }
- }
- this._panning = false;
- },
-
- _onWheel(e) {
- e.preventDefault();
- const rect = this._canvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
-
- const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
- const newZoom = Math.max(0.2, Math.min(3, this._zoom * zoomFactor));
-
- this._offsetX = mx - (mx - this._offsetX) * (newZoom / this._zoom);
- this._offsetY = my - (my - this._offsetY) * (newZoom / this._zoom);
- this._zoom = newZoom;
- },
-
- _onResize() {
- if (!this._canvas || !this._nodes.length) return;
- const w = this._canvas.parentElement.clientWidth;
- const h = this._canvas.parentElement.clientHeight;
- this._canvas.width = w * devicePixelRatio;
- this._canvas.height = h * devicePixelRatio;
- this._canvas.style.width = w + 'px';
- this._canvas.style.height = h + 'px';
- this._width = w;
- this._height = h;
- this._ctx.setTransform(1, 0, 0, 1, 0, 0);
- this._ctx.scale(devicePixelRatio, devicePixelRatio);
- },
-};
-
-// ---------------------------------------------------------------------------
-// Init graph view event listeners
-// ---------------------------------------------------------------------------
-export function initGraphView() {
- const gm = GraphViewManager;
- const closeBtn = document.getElementById('graph-close');
- const zoomIn = document.getElementById('graph-zoom-in');
- const zoomOut = document.getElementById('graph-zoom-out');
- const reset = document.getElementById('graph-reset');
- const fullVault = document.getElementById('graph-full-vault');
- const depthSlider = document.getElementById('graph-depth');
- const searchInput = document.getElementById('graph-search');
- const modal = document.getElementById('graph-modal');
- const canvas = document.getElementById('graph-canvas');
-
- if (closeBtn) closeBtn.addEventListener('click', () => gm.close());
- if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) gm.close(); });
-
- // Navigation buttons
- const navBack = document.getElementById('graph-nav-back');
- if (navBack) navBack.addEventListener('click', () => gm.goBack());
- const navFwd = document.getElementById('graph-nav-fwd');
- if (navFwd) navFwd.addEventListener('click', () => gm.goForward());
- const navUp = document.getElementById('graph-nav-up');
- if (navUp) navUp.addEventListener('click', () => gm.goUp());
-
- if (zoomIn) zoomIn.addEventListener('click', () => { gm._zoom = Math.min(3, gm._zoom * 1.2); });
- if (zoomOut) zoomOut.addEventListener('click', () => { gm._zoom = Math.max(0.2, gm._zoom * 0.8); });
- if (reset) reset.addEventListener('click', () => { gm._offsetX = 0; gm._offsetY = 0; gm._zoom = 1; });
- if (fullVault) fullVault.addEventListener('click', () => gm.toggleScope());
- if (depthSlider) depthSlider.addEventListener('input', () => gm.setDepth(parseInt(depthSlider.value)));
- if (searchInput) {
- let debounce;
- searchInput.addEventListener('input', () => {
- clearTimeout(debounce);
- debounce = setTimeout(() => gm.setSearch(searchInput.value.trim()), 300);
- });
- }
-
- // Export PNG
- const exportBtn = document.getElementById('graph-export');
- if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());
-
- // Fullscreen
- const fullscreenBtn = document.getElementById('graph-fullscreen');
- if (fullscreenBtn) fullscreenBtn.addEventListener('click', () => gm.toggleFullscreen());
-
- // Type filters
- ['dir', 'md', 'other'].forEach(type => {
- const cb = document.getElementById(`graph-filter-${type}`);
- if (cb) cb.addEventListener('change', () => gm.applyTypeFilter());
- });
-
- if (canvas) {
- canvas.addEventListener('mousedown', (e) => gm._onMouseDown(e));
- canvas.addEventListener('mousemove', (e) => gm._onMouseMove(e));
- canvas.addEventListener('mouseup', (e) => gm._onMouseUp(e));
- canvas.addEventListener('mouseleave', () => {
- gm._dragging = false;
- gm._dragNode = null;
- gm._panning = false;
- canvas.style.cursor = 'grab';
- // Panels stay visible until closed via button
- });
- canvas.addEventListener('wheel', (e) => gm._onWheel(e), { passive: false });
- window.addEventListener('resize', () => gm._onResize());
- }
-
- document.addEventListener('keydown', (e) => {
- if (modal && modal.classList.contains('active') && e.key === 'Escape') {
- gm.close();
- }
- });
-
- // Close buttons for sticky panels
- const infoClose = document.getElementById('graph-info-close');
- if (infoClose) infoClose.addEventListener('click', () => { if (gm._infoPanel) gm._infoPanel.style.display = 'none'; });
- const previewClose = document.getElementById('graph-preview-close');
- if (previewClose) previewClose.addEventListener('click', () => { if (gm._previewPanel) gm._previewPanel.style.display = 'none'; });
-}
diff --git a/desktop/frontend-static/js/i18n.js b/desktop/frontend-static/js/i18n.js
deleted file mode 100644
index 0e49e4b..0000000
--- a/desktop/frontend-static/js/i18n.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/* ObsiGate — i18n (internationalisation).
- *
- * Architecture:
- * Flat key→string JSON locale files in /static/locales/{fr,en,...}.json
- * Static DOM: data-i18n="key" → textContent
- * data-i18n-placeholder="key" → placeholder
- * data-i18n-attr:title="key" → title attribute
- * data-i18n-html="key" → innerHTML (use sparingly)
- * Dynamic JS: import { t } from './i18n.js'; t('key', {param: 'val'})
- * Live reload: setLocale('en') updates every data-i18n element instantly.
- *
- * Key naming: section.what — lowercase, dot-separated, descriptive.
- * e.g. header.search_placeholder button.save error.network
- *
- * Usage:
- * import { t, setLocale, getLocale, initI18n } from './i18n.js';
- * await initI18n(); // early, before any rendering
- * el.textContent = t('button.cancel'); // dynamic string
- * setLocale('en'); // user switches language
- */
-
-// ── State ──────────────────────────────────────────────────────────────────
-let _currentLocale = 'fr';
-let _strings = {}; // { key: translated_string }
-let _fallbackStrings = {}; // FR fallback, loaded once
-let _loaded = false;
-
-// ── Public helpers ─────────────────────────────────────────────────────────
-
-/** Return the currently active locale code ('fr', 'en', ...). */
-export function getLocale() {
- return _currentLocale;
-}
-
-/** Translate a key. If params provided, replace {name} placeholders.
- * Falls back: key → French → key itself. Never throws. */
-export function t(key, params) {
- let s = _strings[key];
- if (s === undefined) {
- s = _fallbackStrings[key];
- if (s === undefined) {
- console.warn('obsigate i18n: missing key', key);
- s = key; // last resort — show the key so the dev notices
- }
- }
- if (params) {
- for (const [k, v] of Object.entries(params)) {
- s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), String(v));
- }
- }
- return s;
-}
-
-// ── Reverse-lookup cache (French text → key) ────────────────────────────
-let _reverseCache = null;
-
-function _buildReverseCache() {
- _reverseCache = {};
- for (const [k, v] of Object.entries(_fallbackStrings)) {
- if (k.startsWith('_')) continue;
- // Prefer shorter keys when multiple keys map to the same French text
- if (!_reverseCache[v] || k.length < _reverseCache[v].length) {
- _reverseCache[v] = k;
- }
- }
-}
-
-/** Translate a French string to the current locale by reverse lookup.
- * "Paramètres client sauvegardés" → fr: same, en: "Client settings saved".
- * If the string is not found in the FR locale, returns it unchanged. */
-export function tr(frenchText) {
- if (!frenchText || _currentLocale === 'fr') return frenchText;
- if (!_reverseCache) _buildReverseCache();
- var key = _reverseCache[frenchText];
- return key ? t(key) : frenchText;
-}
-
-/** Change locale and re-translate the entire DOM.
- * Persists to localStorage. */
-export async function setLocale(locale) {
- locale = locale || 'fr';
- if (!_loaded) {
- // Too early — store intent and apply after init
- try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ }
- return;
- }
- await _loadLocaleStrings(locale);
- _applyDOM();
- document.documentElement.lang = locale;
- try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ }
-}
-
-/** Initialise the i18n system. Call once, early, before any rendering.
- * Priority: server profile > localStorage > navigator.language > 'fr'. */
-export async function initI18n() {
- // 1. Determine locale: server profile > localStorage > navigator.language > 'fr'
- let locale = null;
-
- // Try server-side language preference (survives clear site data)
- try {
- const resp = await fetch('/api/auth/me', { credentials: 'include' });
- if (resp.ok) {
- const user = await resp.json();
- if (user.language === 'en' || user.language === 'fr') {
- locale = user.language;
- }
- }
- } catch (e) { /* not logged in or network error — fall through */ }
-
- // Fall back to localStorage
- if (!locale) {
- try { locale = localStorage.getItem('obsigate-lang'); } catch (e) { /* */ }
- }
-
- // Fall back to navigator.language
- if (!locale) {
- const nav = (navigator.language || '').toLowerCase();
- locale = nav.startsWith('fr') ? 'fr'
- : nav.startsWith('en') ? 'en'
- : 'fr';
- }
-
- // 2. Load fallback (FR) — always needed
- _fallbackStrings = await _fetchLocale('fr');
-
- // 3. Load selected locale
- await _loadLocaleStrings(locale);
-
- // 4. Apply to DOM
- _applyDOM();
- document.documentElement.lang = locale;
- _loaded = true;
-}
-
-// ── Internal ───────────────────────────────────────────────────────────────
-
-async function _fetchLocale(locale) {
- try {
- const resp = await fetch('/static/locales/' + locale + '.json');
- if (!resp.ok) throw new Error('HTTP ' + resp.status);
- return await resp.json();
- } catch (err) {
- console.warn('obsigate i18n: failed to load locale ' + locale, err);
- return {};
- }
-}
-
-async function _loadLocaleStrings(locale) {
- _currentLocale = locale;
- if (locale === 'fr') {
- _strings = _fallbackStrings;
- } else {
- _strings = await _fetchLocale(locale);
- }
-}
-
-// ── Global exports for non-module scripts (legacy app.js) ──────────────────
-window.t = t;
-window.tr = tr;
-window.getLocale = getLocale;
-window.setLocale = setLocale;
-window.initI18n = initI18n;
-
-/** Walk the DOM and update every element with a data-i18n* attribute. */
-function _applyDOM() {
- // textContent
- document.querySelectorAll('[data-i18n]').forEach(function (el) {
- const key = el.getAttribute('data-i18n');
- el.textContent = t(key);
- });
-
- // placeholder
- document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
- const key = el.getAttribute('data-i18n-placeholder');
- el.setAttribute('placeholder', t(key));
- });
-
- // innerHTML (use sparingly — XSS risk if keys ever hold user content)
- document.querySelectorAll('[data-i18n-html]').forEach(function (el) {
- const key = el.getAttribute('data-i18n-html');
- el.innerHTML = t(key);
- });
-
- // data-i18n-attr:TITLE → sets any attribute
- document.querySelectorAll('[data-i18n-attr]').forEach(function (el) {
- const raw = el.getAttribute('data-i18n-attr');
- const colon = raw.indexOf(':');
- if (colon === -1) return;
- const attr = raw.substring(0, colon);
- const key = raw.substring(colon + 1);
- el.setAttribute(attr, t(key));
- });
-}
diff --git a/desktop/frontend-static/js/legacy.js b/desktop/frontend-static/js/legacy.js
deleted file mode 100644
index ba4e83b..0000000
--- a/desktop/frontend-static/js/legacy.js
+++ /dev/null
@@ -1,417 +0,0 @@
-/* ObsiGate — Legacy module: remaining functions for the orchestrator
- Extracted from the monolithic frontend/app.js IIFE.
- Functions already in other modules are re-exported. */
-
-import { api } from './auth.js';
-import { state } from './state.js';
-import { escapeHtml, safeCreateIcons } from './utils.js';
-import { TabManager, closeMobileSidebar } from './ui.js';
-import { showProgressBar, hideProgressBar, showWelcome } from './viewer.js';
-
-// --- Search imports ---
-import {
- AutocompleteDropdown,
- SearchChips,
- SearchHistory,
- performAdvancedSearch,
-} from './search.js';
-
-// --- Dashboard imports ---
-import {
- DashboardRecentWidget,
- DashboardStatsWidget,
- DashboardBookmarkWidget,
- DashboardSharedWidget,
-} from './dashboard.js';
-
-// --- Re-exports from modules that already have these functions ---
-export { initSidebarTabs, initConfigModal, initHelpModal, initRecentTab, initAboutModal } from './config.js';
-export { initSyncStatus } from './sync.js';
-export { DashboardRecentWidget } from './dashboard.js';
-
-
-// =========================================================================
-// Helpers
-// =========================================================================
-
-async function loadVaultSettings() {
- try {
- const settings = await api("/api/vaults/settings/all");
- state.vaultSettings = settings;
- } catch (err) {
- console.error("Failed to load vault settings:", err);
- state.vaultSettings = {};
- }
-}
-
-// =========================================================================
-// Config helpers (needed by initSearch)
-// =========================================================================
-
-const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
-
-function _getFrontendConfig() {
- try {
- return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
- } catch {
- return {};
- }
-}
-
-function _getEffective(key, fallback) {
- const cfg = _getFrontendConfig();
- return cfg[key] !== undefined ? cfg[key] : fallback;
-}
-
-/** Check if user is focused on an input/textarea/contenteditable */
-function _isInputFocused() {
- const tag = document.activeElement?.tagName;
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
- return document.activeElement?.isContentEditable === true;
-}
-
-// =========================================================================
-// initSearch
-// =========================================================================
-
-function initSearch() {
- const input = document.getElementById("search-input");
- if (!input) return;
- const caseBtn = document.getElementById("rb-case");
- const wordBtn = document.getElementById("rb-word");
- const regexBtn = document.getElementById("rb-regex");
- const filterBtn = document.getElementById("search-filter-btn");
- const clearBtn = document.getElementById("search-clear-btn");
- const filterRow = document.getElementById("search-filter-row");
- const prevBtn = document.getElementById("rb-prev");
- const nextBtn = document.getElementById("rb-next");
- const countEl = document.getElementById("rb-count");
- const resultsBar = document.getElementById("results-bar");
-
- function _showResultsBar() { if (resultsBar) resultsBar.style.display = 'flex'; }
- function _hideResultsBar() { if (resultsBar) resultsBar.style.display = 'none'; }
-
- function _updateToggleUI() {
- if (caseBtn) caseBtn.classList.toggle("active", state.searchCaseSensitive);
- if (wordBtn) wordBtn.classList.toggle("active", state.searchWholeWord);
- if (regexBtn) regexBtn.classList.toggle("active", state.searchRegex);
- if (filterBtn) filterBtn.classList.toggle("active", state.searchFilterVisible);
- }
-
- // Toggle buttons
- if (caseBtn) caseBtn.addEventListener("click", () => { state.searchCaseSensitive = !state.searchCaseSensitive; _updateToggleUI(); _research(); });
- if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); });
- if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); });
- if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
-
- // Result navigation (up/down arrows + Enter)
- let _searchResultIdx = -1;
- let _searchResultItems = [];
-
- function _updateResultHighlight() {
- _searchResultItems.forEach((el, i) => {
- el.classList.toggle("search-result-active", i === _searchResultIdx);
- });
- if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) {
- _searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" });
- }
- const countEl = document.getElementById("rb-count");
- if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`;
- }
-
- function _refreshResultItems() {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- _searchResultIdx = _searchResultItems.length > 0 ? 0 : -1;
- _updateResultHighlight();
- if (_searchResultItems.length > 0) _showResultsBar(); else _hideResultsBar();
- }
-
- window.navigateSearchResults = function(delta) {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- if (_searchResultItems.length === 0) return;
- _searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta));
- _updateResultHighlight();
- };
-
- if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1));
- if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1));
-
- function _research() {
- const q = input.value.trim();
- if (q.length >= _getEffective("min_query_length", 2)) {
- clearTimeout(state.searchTimeout);
- state.searchTimeout = setTimeout(() => {
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- state.advancedSearchOffset = 0;
- performAdvancedSearch(q, vault, tagFilter);
- }, _getEffective("debounce_ms", 300));
- }
- }
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- if (e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
- else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
- else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
- else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
- }
- });
-
- // Initialize sub-controllers
- AutocompleteDropdown.init();
- SearchChips.init();
-
- // Initially hide clear button
- if (clearBtn) clearBtn.style.display = "none";
-
- // Input handler: autocomplete dropdown only (search triggered on Enter or selection)
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
-
- // Show autocomplete dropdown while typing
- AutocompleteDropdown.populate(input.value, input.selectionStart);
-
- // Clear results when input becomes empty
- if (!hasText) {
- SearchChips.clear();
- showWelcome();
- }
- });
-
- // Focus handler: show history dropdown
- input.addEventListener("focus", () => {
- if (input.value.length === 0) {
- const historyItems = SearchHistory.filter("").slice(0, 5);
- if (historyItems.length > 0) {
- AutocompleteDropdown.populate("", 0);
- }
- }
- });
-
- // Keyboard navigation in dropdown
- input.addEventListener("keydown", (e) => {
- if (AutocompleteDropdown.isVisible()) {
- if (e.key === "ArrowDown") {
- e.preventDefault();
- AutocompleteDropdown.navigateDown();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- AutocompleteDropdown.navigateUp();
- } else if (e.key === "Enter") {
- // First: check dropdown suggestions (higher priority than search results)
- if (AutocompleteDropdown.selectActive()) {
- e.preventDefault();
- return;
- }
- // Second: navigate search results if visible
- const results = document.querySelectorAll(".search-result-item");
- if (results.length > 0 && _searchResultIdx >= 0) {
- const el = results[_searchResultIdx];
- if (el) {
- const vault = el.dataset.vault;
- const path = el.dataset.path;
- if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; }
- }
- }
- // Third: execute search
- AutocompleteDropdown.hide();
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- } else if (e.key === "Escape") {
- AutocompleteDropdown.hide();
- e.stopPropagation();
- }
- } else {
- // Dropdown not visible: navigate search results or execute search
- if (e.key === "ArrowDown") {
- if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); }
- } else if (e.key === "ArrowUp") {
- if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); }
- } else if (e.key === "Enter") {
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- }
- }
- });
-
- clearBtn.addEventListener("click", () => {
- input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- state.searchCaseSensitive = false;
- state.searchWholeWord = false;
- state.searchRegex = false;
- _updateToggleUI();
- _hideResultsBar();
- SearchChips.clear();
- AutocompleteDropdown.hide();
- showWelcome();
- });
-
- // Global keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- // Ctrl+K or Cmd+K: focus search
- if ((e.ctrlKey || e.metaKey) && e.key === "k") {
- e.preventDefault();
- input.focus();
- input.select();
- }
- // "/" key: focus search (when not in an input/textarea)
- if (e.key === "/" && !_isInputFocused()) {
- e.preventDefault();
- input.focus();
- }
- // Escape: blur search input and close dropdown
- if (e.key === "Escape" && document.activeElement === input) {
- AutocompleteDropdown.hide();
- input.blur();
- }
- });
-}
-
-function goHome() {
- const searchInput = document.getElementById("search-input");
- if (searchInput) searchInput.value = "";
-
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
-
- state.currentVault = null;
- state.currentPath = null;
- state.showingSource = false;
- state.cachedRawSource = null;
-
- closeMobileSidebar();
- showWelcome();
-}
-
-// =========================================================================
-// loadSavedSearches (needed by showWelcome)
-// =========================================================================
-
-async function loadSavedSearches() {
- const list = document.getElementById("saved-searches-list");
- const empty = document.getElementById("saved-searches-empty");
- if (!list) return;
- try {
- const searches = await api("/api/saved-searches");
- if (!searches.length) {
- list.innerHTML = "";
- if (empty) empty.style.display = "";
- return;
- }
- if (empty) empty.style.display = "none";
- list.innerHTML = searches.map(s => {
- var isDirSearch = (!s.query || s.query.trim() === '') && s.include_paths;
- var displayQuery = isDirSearch ? (s.include_paths || 'Dossier') : escapeHtml(s.query);
- var typeIcon = isDirSearch
- ? ' '
- : ' ';
- const badges = [];
- if (s.case_sensitive) badges.push('Aa ');
- if (s.whole_word) badges.push('wd ');
- if (s.regex) badges.push('.* ');
- const pathFilters = [];
- if (s.include_paths) pathFilters.push(`\u{1F4E5} ${escapeHtml(s.include_paths)} `);
- if (s.exclude_paths) pathFilters.push(`\u{1F4E4} ${escapeHtml(s.exclude_paths)} `);
- const vaultStr = s.vault && s.vault !== "all" ? `\u{1F4C1} ${escapeHtml(s.vault)} ` : "";
- return `
-
-
${displayQuery}
-
- ${badges.join("")} ${vaultStr}
- ${typeIcon}
-
- ${pathFilters.length ? '
' + pathFilters.join(" ") + '
' : ""}
-
✕
-
- `}).join("");
- // Apply saved filter + init filter bar
- if (typeof _applySavedFilter === 'function') _applySavedFilter();
- if (typeof _initSavedFilter === 'function') _initSavedFilter();
- list.querySelectorAll(".saved-search-item").forEach(item => {
- item.addEventListener("click", (e) => {
- if (e.target.classList.contains("saved-search-delete")) return;
- const idx = Array.from(list.children).indexOf(item);
- const s = searches[idx];
- if (!s) return;
- // Apply the saved search
- const input = document.getElementById("search-input");
- if (input) input.value = s.query;
- state.searchCaseSensitive = s.case_sensitive || false;
- state.searchWholeWord = s.whole_word || false;
- state.searchRegex = s.regex || false;
- if (typeof _updateToggleUI === "function") _updateToggleUI();
- if (s.include_paths) {
- const incl = document.getElementById("search-include-input");
- if (incl) incl.value = s.include_paths;
- }
- if (s.exclude_paths) {
- const excl = document.getElementById("search-exclude-input");
- if (excl) excl.value = s.exclude_paths;
- }
- // Execute the search
- AutocompleteDropdown.hide();
- AutocompleteDropdown._suppressNext = true;
- const vault = s.vault || "all";
- if (input) { input.dispatchEvent(new Event("input")); }
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- performAdvancedSearch(s.query, vault, null);
- });
- });
- list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
- loadSavedSearches();
- }));
- safeCreateIcons();
- } catch (err) { /* silently ignore */ }
-}
-
-// =========================================================================
-// initDashboardTabs (needed by showWelcome)
-// =========================================================================
-
-function initDashboardTabs() {
- document.querySelectorAll(".dashboard-tab").forEach(tab => {
- // Remove existing listeners by cloning
- const newTab = tab.cloneNode(true);
- tab.parentNode.replaceChild(newTab, tab);
- newTab.addEventListener("click", function() {
- document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
- document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
- this.classList.add("active");
- const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
- if (panel) panel.classList.add("active");
- });
- });
-}
-
-// =========================================================================
-// Exports
-// =========================================================================
-
-export {
- loadVaultSettings,
- initSearch,
- showWelcome,
- goHome,
-};
diff --git a/desktop/frontend-static/js/mermaid-viewer.js b/desktop/frontend-static/js/mermaid-viewer.js
deleted file mode 100644
index ff4437a..0000000
--- a/desktop/frontend-static/js/mermaid-viewer.js
+++ /dev/null
@@ -1,845 +0,0 @@
-/**
- * Mermaid.js integration for ObsiGate.
- *
- * Handles initialization, rendering, templates, zoom/pan,
- * fullscreen, focus mode, theme sync, and Obsidian compatibility.
- *
- * Usage: import { initMermaid, renderMermaidBlocks } from './mermaid-viewer.js';
- */
-import { safeCreateIcons } from './utils.js';
-
-// ---------------------------------------------------------------------------
-// Configuration (#20)
-// ---------------------------------------------------------------------------
-
-var _mermaidReady = false;
-var _currentTheme = 'default';
-
-export function initMermaid() {
- if (typeof mermaid === 'undefined') {
- console.warn('[Mermaid] library not loaded');
- return;
- }
- mermaid.initialize(_getMermaidConfig(_currentTheme === 'dark'));
- _mermaidReady = true;
-}
-
-export function updateMermaidTheme(isDark) {
- _currentTheme = isDark ? 'dark' : 'default';
- if (_mermaidReady) {
- mermaid.initialize(_getMermaidConfig(isDark));
- }
-
- // Re-render all existing Mermaid containers with the new theme
- var containers = document.querySelectorAll('.mermaid-container');
- for (var i = 0; i < containers.length; i++) {
- reRenderContainer(containers[i]);
- }
-}
-
-// Unified theme config — used by both init and theme toggle
-function _getMermaidConfig(isDark) {
- return {
- startOnLoad: false,
- securityLevel: 'strict',
- theme: 'base',
- fontFamily: 'inherit',
- themeVariables: isDark ? DARK_THEME : LIGHT_THEME,
- };
-}
-
-// Dark theme — optimized for readability on dark backgrounds
-var DARK_THEME = {
- // Flowchart / general shapes
- primaryColor: '#1e3a5f',
- primaryTextColor: '#c8daf0',
- primaryBorderColor: '#4a7ab5',
- lineColor: '#5a7a9a',
- secondaryColor: '#2a4a6a',
- tertiaryColor: '#3a6090',
- // Background & containers
- background: '#0f1117',
- mainBkg: '#1a2030',
- nodeBorder: '#4a7ab5',
- clusterBkg: '#1a2235',
- clusterBorder: '#3a5a80',
- titleColor: '#a0c0e0',
- edgeLabelBackground: '#1a2d47',
- nodeTextColor: '#c8daf0',
- // Sequence diagram
- actorBkg: '#1e3a5f',
- actorBorder: '#4a7ab5',
- actorTextColor: '#c8daf0',
- actorLineColor: '#4a7ab5',
- signalColor: '#c8daf0',
- signalTextColor: '#c8daf0',
- labelBoxBkgColor: '#1e3a5f',
- labelBoxBorderColor: '#4a7ab5',
- labelTextColor: '#c8daf0',
- loopTextColor: '#c8daf0',
- noteBkgColor: '#2a3550',
- noteBorderColor: '#4a7ab5',
- noteTextColor: '#c8daf0',
- activationBkgColor: '#3a5a80',
- // Gantt
- sectionBkgColor: '#1a2d47',
- altSectionBkgColor: '#1e3350',
- sectionBkgColor2: '#1a2d47',
- taskBkgColor: '#1e3a5f',
- taskBorderColor: '#4a7ab5',
- taskTextColor: '#c8daf0',
- taskTextOutsideColor: '#8aacca',
- taskTextLightColor: '#e0e8f0',
- gridColor: '#2a3a55',
- activeTaskBkgColor: '#2a5a8a',
- activeTaskBorderColor: '#6a9ad0',
- doneTaskBkgColor: '#1a3a4f',
- doneTaskBorderColor: '#3a7a9a',
- critBkgColor: '#5a2a3a',
- critBorderColor: '#a04050',
- todayLineColor: '#4a7ab5',
- // Git graph
- git0: '#4a7ab5',
- git1: '#3a6a9a',
- git2: '#5a8ac0',
- git3: '#2a5a8a',
- git4: '#6a9ad0',
- git5: '#3a7aaa',
- git6: '#4a8abf',
- git7: '#5a9acf',
- gitInv0: '#c8daf0',
- gitInv1: '#b0c8e0',
- gitInv2: '#d0e0f0',
- gitInv3: '#a0b8d0',
- gitBranchLabel0: '#c8daf0',
- gitBranchLabel1: '#b0c8e0',
- gitBranchLabel2: '#d0e0f0',
- commitLabelColor: '#c8daf0',
- commitLabelBackground: '#1e3a5f',
- commitLabelFontSize: '11px',
- tagLabelColor: '#a0c0e0',
- tagLabelBackground: '#1a2d47',
- tagLabelBorder: '#4a7ab5',
- // Pie chart
- pie1: '#1e3a5f',
- pie2: '#2a5a8a',
- pie3: '#153050',
- pie4: '#3a6a9a',
- pie5: '#204070',
- pie6: '#4a7ab5',
- pie7: '#255080',
- pie8: '#5a8ac0',
- pieTitleTextColor: '#c8daf0',
- pieSectionTextColor: '#c8daf0',
- pieLegendTextColor: '#a0c0e0',
- pieStrokeColor: '#0f1117',
- // Class diagram
- classText: '#c8daf0',
- // State diagram
- labelColor: '#c8daf0',
- // ER diagram — dark alternating rows for readability
- fillType0: '#111d30',
- fillType1: '#152238',
- fillType2: '#111d30',
- fillType3: '#152238',
- fillType4: '#111d30',
- fillType5: '#152238',
- fillType6: '#111d30',
- fillType7: '#152238',
- entityBorder: '#4a7ab5',
- entityBackground: '#0d1520',
- attributeEvenColor: '#ffffff',
- attributeOddColor: '#c8daf0',
- // Timeline — section colors
- cScale0: '#1e3a5f',
- cScale1: '#2a5a8a',
- cScale2: '#153050',
- cScale3: '#3a6a9a',
- cScale4: '#204070',
- cScale5: '#4a7ab5',
- cScaleLabel0: '#c8daf0',
- cScaleLabel1: '#c8daf0',
- cScaleLabel2: '#c8daf0',
- cScaleLabel3: '#c8daf0',
- cScaleLabel4: '#c8daf0',
- cScaleLabel5: '#c8daf0',
- // XY Chart — bar and line colors
- xyChartBackgroundColor: '#0f1117',
- xyChartTitleColor: '#c8daf0',
- xyChartXaxisTitleColor: '#8aacca',
- xyChartYaxisTitleColor: '#8aacca',
- plotColor1: '#1e3a5f',
- plotColor2: '#4a7ab5',
- plotColor3: '#2a5a8a',
- plotColor4: '#5a8ac0',
- plotColor5: '#3a6a9a',
- plotColor6: '#6a9ad0',
- plotColor7: '#153050',
- plotColor8: '#204070',
- // General text
- textColor: '#c8daf0',
- fontSize: '14px',
-};
-
-// Light theme — clean, minimal
-var LIGHT_THEME = {
- primaryColor: '#f0f0f5',
- primaryTextColor: '#1a1a2e',
- primaryBorderColor: '#ccc',
- lineColor: '#666',
- secondaryColor: '#e8e8f0',
- tertiaryColor: '#fafafa',
- background: '#ffffff',
- mainBkg: '#f5f5fa',
- nodeBorder: '#ccc',
- clusterBkg: '#f0f0f5',
- clusterBorder: '#ddd',
- titleColor: '#333',
- edgeLabelBackground: '#f5f5fa',
- nodeTextColor: '#1a1a2e',
- fontSize: '14px',
-};
-
-// ---------------------------------------------------------------------------
-// Templates for all diagram types (#22)
-// ---------------------------------------------------------------------------
-
-export var MERMAID_TEMPLATES = {
- flowchart: 'flowchart TD\n A[Start] --> B{Decision?}\n B -->|Yes| C[OK]\n B -->|No| D[Cancel]',
- sequenceDiagram: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!',
- classDiagram: 'classDiagram\n class Animal {\n +name: string\n +age: int\n +makeSound()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog',
- 'stateDiagram-v2': 'stateDiagram-v2\n [*] --> Idle\n Idle --> Running : start\n Running --> Idle : stop\n Running --> [*] : done',
- erDiagram: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains',
- journey: 'journey\n title My day\n section Morning\n Wake up: 5: Me\n Breakfast: 3: Me\n section Work\n Code: 5: Me, Cat',
- gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Design\n Mockups :a1, 2024-01-01, 7d\n section Dev\n Backend :a2, after a1, 14d',
- pie: 'pie title Languages\n "Python" : 45\n "JavaScript" : 30\n "Go" : 15\n "Rust" : 10',
- gitGraph: 'gitGraph\n commit\n branch feature\n checkout feature\n commit\n checkout main\n merge feature',
- requirementDiagram: 'requirementDiagram\n requirement req1 {\n id: 1\n text: System shall do X\n }\n element Component {\n type: package\n }\n Component - satisfies -> req1',
- mindmap: 'mindmap\n root((Project))\n Frontend\n HTML\n CSS\n JavaScript\n Backend\n API\n Database',
- timeline: 'timeline\n title History\n 2020 : Started\n 2022 : v1.0\n 2024 : v2.0',
- quadrantChart: 'quadrantChart\n title Products\n x-axis Low Reach --> High Reach\n y-axis Low Value --> High Value\n quadrant-1 Stars\n quadrant-2 Question Marks\n Product A: [0.8, 0.6]\n Product B: [0.3, 0.8]',
- C4Context: 'C4Context\n title System Context\n Person(user, "User")\n System(app, "My App")\n System_Ext(db, "Database")\n Rel(user, app, "Uses")\n Rel(app, db, "Reads/Writes")',
- C4Container: 'C4Container\n title Containers\n Container(web, "Web App", "React")\n Container(api, "API", "FastAPI")\n ContainerDb(db, "DB", "PostgreSQL")\n Rel(web, api, "REST")\n Rel(api, db, "SQL")',
- C4Component: 'C4Component\n title API Components\n Container_Boundary(api, "API") {\n Component(auth, "Auth Service")\n Component(search, "Search Engine")\n }',
- C4Dynamic: 'C4Dynamic\n title Login Flow\n Person(user, "User")\n Container(web, "Web App")\n Container(api, "API")\n Rel(user, web, "POST /login")\n Rel(web, api, "authenticate")',
- C4Deployment: 'C4Deployment\n title Deployment\n Deployment_Node(cloud, "Cloud") {\n Container(web, "Web")\n Container(api, "API")\n }',
- 'xychart-beta': 'xychart-beta\n title "Sales"\n x-axis [Jan, Feb, Mar, Apr]\n y-axis "Revenue" 0 --> 100\n bar [45, 60, 75, 90]\n line [30, 45, 55, 70]',
- 'block-beta': 'block-beta\n columns 3\n A B C\n D E F',
- 'sankey-beta': 'sankey-beta\n A, B, 50\n A, C, 30\n B, D, 20\n C, D, 30',
-};
-
-// ---------------------------------------------------------------------------
-// Rendering engine (#21)
-// ---------------------------------------------------------------------------
-
-var _renderCounter = 0;
-
-export async function renderMermaidBlocks(container) {
- if (!_mermaidReady || typeof mermaid === 'undefined') return;
-
- var blocks = container.querySelectorAll('pre code.language-mermaid, .mermaid-code');
- if (blocks.length === 0) return;
-
- for (var i = 0; i < blocks.length; i++) {
- var codeBlock = blocks[i];
- var originalCode = codeBlock.textContent || codeBlock.innerText || '';
- if (!originalCode.trim()) continue;
-
- var wrapper = codeBlock.closest('pre') || codeBlock.parentElement;
- if (!wrapper) continue;
-
- // Check if already rendered
- if (wrapper.querySelector('.mermaid-rendered')) continue;
-
- try {
- // Preprocess Obsidian syntax before Mermaid parsing (#24)
- var code = preprocessObsidian(originalCode);
- await mermaid.parse(code);
- var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
- var result = await mermaid.render(id, code);
-
- // Extract diagram type from first non-comment, non-empty line
- var diagramType = extractDiagramType(code);
-
- // Build container with header + body
- var containerDiv = document.createElement('div');
- containerDiv.className = 'mermaid-container';
- containerDiv.setAttribute('data-mermaid-code', originalCode);
-
- // Header bar
- var header = document.createElement('div');
- header.className = 'mermaid-header';
- header.innerHTML =
- '' + escapeHtml(diagramType) + ' ' +
- '';
-
- // Body
- var body = document.createElement('div');
- body.className = 'mermaid-body';
- body.innerHTML =
- '' + result.svg + '
' +
- '' + escapeHtml(originalCode) + ' ';
-
- containerDiv.appendChild(header);
- containerDiv.appendChild(body);
-
- wrapper.parentNode.replaceChild(containerDiv, wrapper);
-
- // Wire up header buttons
- wireHeaderButtons(containerDiv, originalCode);
- setupZoomPan(containerDiv);
- setupCodePreviewToggle(containerDiv);
- // Patch beta diagram colors for dark mode
- if (_currentTheme === 'dark') patchBetaColors(containerDiv, diagramType);
- safeCreateIcons();
- } catch (err) {
- var errorDiv = document.createElement('div');
- errorDiv.className = 'mermaid-error';
- errorDiv.innerHTML = 'Erreur Mermaid ' + escapeHtml(err.message || String(err));
- wrapper.parentNode.replaceChild(errorDiv, wrapper);
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-// Header button wiring
-// ---------------------------------------------------------------------------
-
-function wireHeaderButtons(container, originalCode) {
- // Copy SVG
- var copySvgBtn = container.querySelector('.mermaid-copy-svg');
- if (copySvgBtn) {
- copySvgBtn.addEventListener('click', function() {
- copySvgToClipboard(container);
- });
- }
-
- // Copy code
- var copyCodeBtn = container.querySelector('.mermaid-copy-code');
- if (copyCodeBtn) {
- copyCodeBtn.addEventListener('click', function() {
- copyToClipboard(originalCode);
- });
- }
-
- // Download PNG
- var downloadBtn = container.querySelector('.mermaid-download');
- if (downloadBtn) {
- downloadBtn.addEventListener('click', function() {
- exportMermaidPNG(container);
- });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Code / Preview toggle
-// ---------------------------------------------------------------------------
-
-function setupCodePreviewToggle(container) {
- var toggleBtns = container.querySelectorAll('.mermaid-toggle-btn');
- var rendered = container.querySelector('.mermaid-rendered');
- var source = container.querySelector('.mermaid-source');
- var zoomBtns = container.querySelectorAll('.mermaid-zoom-in, .mermaid-zoom-out');
- var fullscreenBtn = container.querySelector('.mermaid-fullscreen');
-
- for (var i = 0; i < toggleBtns.length; i++) {
- toggleBtns[i].addEventListener('click', function() {
- var mode = this.getAttribute('data-mode');
-
- // Update active state
- for (var j = 0; j < toggleBtns.length; j++) {
- toggleBtns[j].classList.remove('active');
- }
- this.classList.add('active');
-
- if (mode === 'preview') {
- rendered.style.display = '';
- source.style.display = 'none';
- // Show zoom/fullscreen buttons in preview mode
- for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = '';
- if (fullscreenBtn) fullscreenBtn.style.display = '';
- } else {
- rendered.style.display = 'none';
- source.style.display = '';
- // Hide zoom/fullscreen in code mode
- for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = 'none';
- if (fullscreenBtn) fullscreenBtn.style.display = 'none';
- }
- });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Diagram type extraction
-// ---------------------------------------------------------------------------
-
-function extractDiagramType(code) {
- var lines = code.trim().split('\n');
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i].trim();
- // Skip comments and empty lines
- if (!line || line.indexOf('%%') === 0) continue;
- // Return the first meaningful word(s)
- var words = line.split(/\s+/);
- if (words.length >= 2) {
- return words[0] + ' ' + words[1];
- }
- return words[0] || 'mermaid';
- }
- return 'mermaid';
-}
-
-// ---------------------------------------------------------------------------
-// Re-render a container after theme change
-// ---------------------------------------------------------------------------
-
-async function reRenderContainer(container) {
- var code = container.getAttribute('data-mermaid-code');
- if (!code) return;
-
- var rendered = container.querySelector('.mermaid-rendered');
- if (!rendered) return;
-
- try {
- var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
- var result = await mermaid.render(id, preprocessObsidian(code));
- rendered.innerHTML = result.svg;
-
- // Reset zoom/pan state on the SVG
- var svg = rendered.querySelector('svg');
- if (svg) {
- svg.style.transform = '';
- svg.style.cursor = 'grab';
- }
- // Patch beta diagram colors for dark mode
- if (_currentTheme === 'dark') {
- var diagramType = extractDiagramType(code);
- patchBetaColors(container, diagramType);
- }
- } catch (err) {
- // Keep old rendering on error
- console.warn('[Mermaid] Re-render failed:', err.message);
- }
-}
-
-// ---------------------------------------------------------------------------
-// Patch beta diagram colors for dark mode (inline SVG attributes beat CSS)
-// ---------------------------------------------------------------------------
-
-function patchBetaColors(container, diagramType) {
- var svg = container.querySelector('.mermaid-rendered svg');
- if (!svg) return;
-
- // packet-beta — override white block backgrounds
- if (diagramType.indexOf('packet') !== -1) {
- var rects = svg.querySelectorAll('rect');
- for (var i = 0; i < rects.length; i++) {
- var r = rects[i];
- var fill = r.getAttribute('fill');
- // Only override if the fill is light/white (preserve dark fills)
- if (!fill || fill === '#f4f4f4' || fill === '#fff' || fill === '#ffffff' || fill === 'white' || fill === '#e8e8e8') {
- r.setAttribute('fill', '#1e3a5f');
- r.setAttribute('stroke', '#4a7ab5');
- }
- }
- // Text color
- var texts = svg.querySelectorAll('text');
- for (var j = 0; j < texts.length; j++) {
- var t = texts[j];
- var tfill = t.getAttribute('fill');
- if (!tfill || tfill === '#000' || tfill === '#333' || tfill === 'black') {
- t.setAttribute('fill', '#c8daf0');
- }
- }
- }
-
- // xychart-beta — override cream bars and orange line
- if (diagramType.indexOf('xychart') !== -1) {
- var allRects = svg.querySelectorAll('rect');
- for (var k = 0; k < allRects.length; k++) {
- var rr = allRects[k];
- var f = rr.getAttribute('fill');
- if (!f || f === '#f4f4f4' || f === '#fff' || f === '#ffffff' || f === 'white' || f.indexOf('cream') !== -1 || f === '#ececec') {
- rr.setAttribute('fill', '#1e3a5f');
- rr.setAttribute('stroke', '#4a7ab5');
- }
- }
- // Line color
- var paths = svg.querySelectorAll('path');
- for (var m = 0; m < paths.length; m++) {
- var p = paths[m];
- var ps = p.getAttribute('stroke');
- if (!ps || ps === '#ffa500' || ps === 'orange' || ps === '#ff8c00') {
- p.setAttribute('stroke', '#6a9ad0');
- }
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-// Zoom & Pan (#27)
-// ---------------------------------------------------------------------------
-
-function setupZoomPan(container) {
- var svgWrapper = container.querySelector('.mermaid-rendered');
- if (!svgWrapper) return;
- var svg = svgWrapper.querySelector('svg');
- if (!svg) return;
-
- var scale = 1;
- var panX = 0, panY = 0;
- var dragging = false, startX, startY;
-
- svg.style.transformOrigin = '0 0';
- svg.style.cursor = 'grab';
-
- function update() {
- svg.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + ')';
- }
-
- svg.addEventListener('mousedown', function(e) {
- dragging = true;
- startX = e.clientX - panX;
- startY = e.clientY - panY;
- svg.style.cursor = 'grabbing';
- e.preventDefault();
- });
-
- document.addEventListener('mousemove', function(e) {
- if (!dragging) return;
- panX = e.clientX - startX;
- panY = e.clientY - startY;
- update();
- });
-
- document.addEventListener('mouseup', function() {
- dragging = false;
- if (svg) svg.style.cursor = 'grab';
- });
-
- svg.addEventListener('wheel', function(e) {
- e.preventDefault();
- var delta = e.deltaY > 0 ? 0.9 : 1.1;
- scale = Math.max(0.2, Math.min(5, scale * delta));
- update();
- }, { passive: false });
-
- var zoomIn = container.querySelector('.mermaid-zoom-in');
- var zoomOut = container.querySelector('.mermaid-zoom-out');
- if (zoomIn) zoomIn.addEventListener('click', function() { scale = Math.min(5, scale * 1.3); update(); });
- if (zoomOut) zoomOut.addEventListener('click', function() { scale = Math.max(0.2, scale * 0.7); update(); });
-
- // Fullscreen (#28)
- var fsBtn = container.querySelector('.mermaid-fullscreen');
- if (fsBtn) {
- fsBtn.addEventListener('click', function() {
- openFullscreen(container);
- });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Fullscreen mode (#28) — with header bar + full-size diagram
-// ---------------------------------------------------------------------------
-
-function openFullscreen(container) {
- var svgWrapper = container.querySelector('.mermaid-rendered');
- var diagramType = container.querySelector('.mermaid-diagram-type');
- var typeLabel = diagramType ? diagramType.textContent : 'mermaid';
-
- var overlay = document.createElement('div');
- overlay.className = 'mermaid-fullscreen-overlay';
-
- // Fullscreen header bar
- overlay.innerHTML =
- '' +
- '
';
-
- var content = overlay.querySelector('.mermaid-fullscreen-content');
- content.innerHTML = svgWrapper.innerHTML;
- var svg = content.querySelector('svg');
- if (svg) {
- svg.style.cursor = 'default';
- }
-
- document.body.appendChild(overlay);
-
- // Fullscreen zoom
- var fsScale = 1, fsPanX = 0, fsPanY = 0, fsDragging = false, fsStartX, fsStartY;
- if (svg) {
- svg.style.transformOrigin = '0 0';
- function fsUpdate() {
- svg.style.transform = 'translate(' + fsPanX + 'px, ' + fsPanY + 'px) scale(' + fsScale + ')';
- }
-
- svg.addEventListener('mousedown', function(e) {
- fsDragging = true;
- fsStartX = e.clientX - fsPanX;
- fsStartY = e.clientY - fsPanY;
- svg.style.cursor = 'grabbing';
- e.preventDefault();
- });
-
- document.addEventListener('mousemove', function(e) {
- if (!fsDragging) return;
- fsPanX = e.clientX - fsStartX;
- fsPanY = e.clientY - fsStartY;
- fsUpdate();
- });
-
- document.addEventListener('mouseup', function() {
- fsDragging = false;
- if (svg) svg.style.cursor = 'grab';
- });
-
- svg.addEventListener('wheel', function(e) {
- e.preventDefault();
- var delta = e.deltaY > 0 ? 0.9 : 1.1;
- fsScale = Math.max(0.2, Math.min(5, fsScale * delta));
- fsUpdate();
- }, { passive: false });
-
- var fsZoomIn = overlay.querySelector('.mermaid-fs-zoom-in');
- var fsZoomOut = overlay.querySelector('.mermaid-fs-zoom-out');
- if (fsZoomIn) fsZoomIn.addEventListener('click', function() { fsScale = Math.min(5, fsScale * 1.3); fsUpdate(); });
- if (fsZoomOut) fsZoomOut.addEventListener('click', function() { fsScale = Math.max(0.2, fsScale * 0.7); fsUpdate(); });
- }
-
- // Copy SVG in fullscreen
- var fsCopySvg = overlay.querySelector('.mermaid-fs-copy-svg');
- if (fsCopySvg) {
- fsCopySvg.addEventListener('click', function() {
- copySvgFromElement(content);
- });
- }
-
- // Copy code in fullscreen
- var fsCopyCode = overlay.querySelector('.mermaid-fs-copy-code');
- if (fsCopyCode) {
- fsCopyCode.addEventListener('click', function() {
- var code = container.getAttribute('data-mermaid-code') || '';
- copyToClipboard(code);
- });
- }
-
- // Download PNG in fullscreen
- var fsDownload = overlay.querySelector('.mermaid-fs-download');
- if (fsDownload) {
- fsDownload.addEventListener('click', function() {
- exportMermaidPNGFromElement(svg);
- });
- }
-
- safeCreateIcons();
-
- function close() {
- overlay.remove();
- document.removeEventListener('keydown', escHandler);
- }
-
- var escHandler = function(e) {
- if (e.key === 'Escape') close();
- };
- document.addEventListener('keydown', escHandler);
-
- overlay.addEventListener('click', function(e) {
- if (e.target === overlay) close();
- });
- overlay.querySelector('.mermaid-fs-close').addEventListener('click', close);
-}
-
-// ---------------------------------------------------------------------------
-// Copy SVG to clipboard
-// ---------------------------------------------------------------------------
-
-function copySvgToClipboard(container) {
- copySvgFromElement(container.querySelector('.mermaid-rendered'));
-}
-
-function copySvgFromElement(el) {
- if (!el) return;
- var svg = el.querySelector('svg');
- if (!svg) return;
- var data = new XMLSerializer().serializeToString(svg);
- copyToClipboard(data);
-}
-
-function fallbackCopy(text) {
- var textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.style.position = 'fixed';
- textarea.style.opacity = '0';
- document.body.appendChild(textarea);
- textarea.select();
- try { document.execCommand('copy'); } catch (e) {}
- document.body.removeChild(textarea);
-}
-
-// ---------------------------------------------------------------------------
-// Export SVG/PNG (#24)
-// ---------------------------------------------------------------------------
-
-export function exportMermaidSVG(container) {
- var svg = container.querySelector('.mermaid-rendered svg');
- if (!svg) return;
- var data = new XMLSerializer().serializeToString(svg);
- var blob = new Blob([data], { type: 'image/svg+xml' });
- downloadBlob(blob, 'diagram.svg');
-}
-
-export function exportMermaidPNG(container) {
- var svg = container.querySelector('.mermaid-rendered svg');
- exportMermaidPNGFromElement(svg);
-}
-
-function exportMermaidPNGFromElement(svg) {
- if (!svg) return;
- var data = new XMLSerializer().serializeToString(svg);
- var canvas = document.createElement('canvas');
- var ctx = canvas.getContext('2d');
- var img = new Image();
- var blob = new Blob([data], { type: 'image/svg+xml' });
- var url = URL.createObjectURL(blob);
-
- img.onload = function() {
- canvas.width = img.width * 2;
- canvas.height = img.height * 2;
- ctx.scale(2, 2);
- ctx.drawImage(img, 0, 0);
- URL.revokeObjectURL(url);
- canvas.toBlob(function(pngBlob) {
- downloadBlob(pngBlob, 'diagram.png');
- }, 'image/png');
- };
- img.src = url;
-}
-
-function downloadBlob(blob, filename) {
- var a = document.createElement('a');
- a.href = URL.createObjectURL(blob);
- a.download = filename;
- a.click();
- URL.revokeObjectURL(a.href);
-}
-
-// ---------------------------------------------------------------------------
-// Copy text to clipboard
-// ---------------------------------------------------------------------------
-
-function copyToClipboard(text) {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text).catch(function() {
- fallbackCopy(text);
- });
- } else {
- fallbackCopy(text);
- }
-}
-
-// ---------------------------------------------------------------------------
-// Focus mode (#29) — click diagram to enlarge in a side panel
-// ---------------------------------------------------------------------------
-
-var _focusPanel = null;
-
-export function setupFocusMode() {
- document.addEventListener('click', function(e) {
- var container = e.target.closest('.mermaid-container');
- if (!container) return;
- // Don't trigger focus if clicking header or controls
- if (e.target.closest('.mermaid-header') || e.target.closest('button')) return;
-
- var svgWrapper = container.querySelector('.mermaid-rendered');
- if (!svgWrapper) return;
-
- if (_focusPanel && _focusPanel.style.display !== 'none') {
- updateFocusPanel(svgWrapper);
- } else {
- showFocusPanel(svgWrapper);
- }
- });
-}
-
-function showFocusPanel(svgWrapper) {
- if (!_focusPanel) {
- _focusPanel = document.createElement('div');
- _focusPanel.className = 'mermaid-focus-panel';
- _focusPanel.innerHTML =
- '' +
- '
';
- document.body.appendChild(_focusPanel);
-
- _focusPanel.querySelector('.mermaid-focus-close').addEventListener('click', function() {
- _focusPanel.style.display = 'none';
- });
- }
-
- _focusPanel.style.display = 'block';
- updateFocusPanel(svgWrapper);
- safeCreateIcons();
-}
-
-function updateFocusPanel(svgWrapper) {
- if (!_focusPanel) return;
- var content = _focusPanel.querySelector('.mermaid-focus-content');
- content.innerHTML = svgWrapper.innerHTML;
- var svg = content.querySelector('svg');
- if (svg) {
- svg.style.width = '100%';
- svg.style.height = 'auto';
- svg.style.maxHeight = '80vh';
- }
-}
-
-// ---------------------------------------------------------------------------
-// Obsidian compatibility pre-processor (#24)
-// ---------------------------------------------------------------------------
-
-export function preprocessObsidian(code) {
- return code
- .replace(/!\[\[([^\]]+?)\]\]/g, function(_, m) {
- var parts = m.split('|');
- var src = parts[0].trim();
- var width = parts[1] ? parts[1].trim() : null;
- if (width && /^\d+$/.test(width)) {
- return ' ';
- }
- return ' ';
- })
- .replace(/\[\[([^\]]+?)\]\]/g, '$1 ')
- .replace(/==([^=]+)==/g, '$1 ');
-}
-
-// ---------------------------------------------------------------------------
-// Utility
-// ---------------------------------------------------------------------------
-
-function escapeHtml(str) {
- var div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
-}
diff --git a/desktop/frontend-static/js/mobile-toolbar.js b/desktop/frontend-static/js/mobile-toolbar.js
deleted file mode 100644
index b1adc19..0000000
--- a/desktop/frontend-static/js/mobile-toolbar.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/* ObsiGate — Mobile Bottom Toolbar (Obsidian-style)
- Phases 1-5: fixed bar, vault home, file palette, commands, tab switcher */
-import { state } from './state.js';
-import { TabManager } from './ui.js';
-
-// ── Open file palette (search files) ──
-function openFilePalette() {
- import('./palette.js').then(m => { m.open(''); });
-}
-
-// ── Open command palette ──
-function openCommandPalette() {
- import('./palette.js').then(m => { m.open('> '); });
-}
-
-// ── Tab switcher ──
-function openTabSwitcher() {
- const sheet = document.getElementById('mt-tab-switcher');
- const overlay = document.getElementById('mt-overlay');
- if (!sheet || !overlay) return;
-
- renderTabList();
- sheet.classList.add('open');
- overlay.classList.add('visible');
-}
-
-function closeTabSwitcher() {
- const sheet = document.getElementById('mt-tab-switcher');
- const overlay = document.getElementById('mt-overlay');
- if (sheet) sheet.classList.remove('open');
- if (overlay) overlay.classList.remove('visible');
-}
-
-function renderTabList() {
- const list = document.getElementById('mt-tab-list');
- if (!list) return;
-
- const tabs = TabManager._tabs || [];
- const activeId = TabManager._activeTabId;
-
- if (tabs.length === 0) {
- list.innerHTML = 'Aucun fichier ouvert
';
- return;
- }
-
- list.innerHTML = tabs.map(t => {
- const isActive = t.id === activeId;
- const icon = t.path.endsWith('.md') ? '📄' : '📝';
- return `
- ${icon}
- ${t.title || t.path.split('/').pop()}
- ✕
-
`;
- }).join('');
-
- list.querySelectorAll('.mt-tab-item').forEach(el => {
- el.addEventListener('click', (e) => {
- if (e.target.closest('.mt-tab-item-close')) return;
- const tabId = el.dataset.tabId;
- if (tabId) {
- TabManager.activate(tabId);
- closeTabSwitcher();
- }
- });
- });
-
- list.querySelectorAll('.mt-tab-item-close').forEach(btn => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation();
- const tabId = btn.dataset.close;
- if (tabId) {
- TabManager.close(tabId);
- setTimeout(() => renderTabList(), 100);
- }
- });
- });
-}
-
-function updateTabCount() {
- const badge = document.getElementById('mt-tab-count');
- if (!badge) return;
- const count = (TabManager._tabs || []).length;
- if (count > 0) {
- badge.style.display = 'flex';
- badge.textContent = count;
- } else {
- badge.style.display = 'none';
- }
-}
-
-// ── Vault home / sidebar toggle ──
-function toggleExplorer() {
- const sidebar = document.getElementById('sidebar');
- if (sidebar) {
- if (sidebar.classList.contains('mobile-open')) {
- sidebar.classList.remove('mobile-open');
- } else {
- sidebar.classList.add('mobile-open');
- }
- }
- if (!state.currentVault && state.allVaults && state.allVaults.length > 0) {
- import('./sidebar.js').then(m => {
- if (m.setSelectedVaultContext) m.setSelectedVaultContext(state.allVaults[0].name);
- });
- }
-}
-
-// ── Init ──
-export function initMobileToolbar() {
- const explorerBtn = document.getElementById('mt-explorer');
- const searchBtn = document.getElementById('mt-search');
- const paletteBtn = document.getElementById('mt-palette');
- const tabsBtn = document.getElementById('mt-tabs');
- const sheetClose = document.getElementById('mt-sheet-close');
- const closeAll = document.getElementById('mt-close-all');
- const overlay = document.getElementById('mt-overlay');
-
- if (explorerBtn) explorerBtn.addEventListener('click', toggleExplorer);
- if (searchBtn) searchBtn.addEventListener('click', openFilePalette);
- if (paletteBtn) paletteBtn.addEventListener('click', openCommandPalette);
- if (tabsBtn) tabsBtn.addEventListener('click', openTabSwitcher);
- if (sheetClose) sheetClose.addEventListener('click', closeTabSwitcher);
- if (closeAll) closeAll.addEventListener('click', () => {
- const tabs = [...(TabManager._tabs || [])];
- tabs.forEach(t => TabManager.close(t.id));
- updateTabCount();
- renderTabList();
- });
- if (overlay) overlay.addEventListener('click', closeTabSwitcher);
-
- setInterval(updateTabCount, 2000);
- updateTabCount();
-
- // ── Vault context indicator ──
- function updateVaultPill() {
- const pill = document.getElementById('vault-context-pill');
- if (!pill) return;
- const vaultName = state.selectedContextVault || 'all';
- if (vaultName === 'all') {
- pill.textContent = t("mobile.all_vaults");
- } else {
- pill.textContent = '📂 ' + vaultName;
- }
- }
- updateVaultPill();
- setInterval(updateVaultPill, 1500);
-
- window._mtUpdateTabs = updateTabCount;
-}
diff --git a/desktop/frontend-static/js/offline-db.js b/desktop/frontend-static/js/offline-db.js
deleted file mode 100644
index 94edb14..0000000
--- a/desktop/frontend-static/js/offline-db.js
+++ /dev/null
@@ -1,377 +0,0 @@
-/* ObsiGate — Offline IndexedDB Storage
- * Stores: file index, file contents, pending changes queue
- * Schema v1:
- * files store: { path, vault, title, tags[], snippet, modified, tokens[], weight }
- * content store: { key: "vault::path", content: string, modified }
- * pending store: { id, vault, path, content, action, timestamp }
- */
-
-const DB_NAME = 'obsigate-offline';
-const DB_VERSION = 1;
-
-let _db = null;
-let _dbPromise = null;
-
-export const OfflineDB = {
-
- async open() {
- if (_db) return _db;
- if (_dbPromise) return _dbPromise;
-
- _dbPromise = new Promise((resolve, reject) => {
- const req = indexedDB.open(DB_NAME, DB_VERSION);
- req.onupgradeneeded = (e) => {
- const db = e.target.result;
- console.log('[OfflineDB] Upgrading to v' + DB_VERSION);
-
- // File index store — keyed by composite "vault::path"
- if (!db.objectStoreNames.contains('files')) {
- const store = db.createObjectStore('files', { keyPath: 'key' });
- store.createIndex('vault', 'vault', { unique: false });
- store.createIndex('modified', 'modified', { unique: false });
- store.createIndex('title', 'title', { unique: false });
- }
-
- // File contents store — keyed by "vault::path"
- if (!db.objectStoreNames.contains('content')) {
- db.createObjectStore('content', { keyPath: 'key' });
- }
-
- // Pending changes queue — auto-increment key
- if (!db.objectStoreNames.contains('pending')) {
- const store = db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
- store.createIndex('timestamp', 'timestamp', { unique: false });
- }
- };
-
- req.onsuccess = (e) => {
- _db = e.target.result;
- console.log('[OfflineDB] Database opened');
- resolve(_db);
- };
-
- req.onerror = () => {
- _dbPromise = null;
- reject(req.error);
- };
- });
-
- return _dbPromise;
- },
-
- /* ── File Index ── */
-
- async putFileIndex(file) {
- const db = await this.open();
- return new Promise((resolve, reject) => {
- const tx = db.transaction('files', 'readwrite');
- const store = tx.objectStore('files');
- const key = file.vault + '::' + file.path;
- const doc = { key: key, vault: file.vault, path: file.path,
- title: file.title, tags: file.tags || [], snippet: file.snippet || '',
- modified: file.modified || '', tokens: file.tokens || [], weight: file.score || 0 };
- store.put(doc);
- tx.oncomplete = () => resolve();
- tx.onerror = () => reject(tx.error);
- });
- },
-
- async putFileIndexBatch(files) {
- const db = await this.open();
- return new Promise((resolve, reject) => {
- const tx = db.transaction('files', 'readwrite');
- const store = tx.objectStore('files');
- for (const file of files) {
- const key = file.vault + '::' + file.path;
- store.put({
- key: key, vault: file.vault, path: file.path,
- title: file.title, tags: file.tags || [], snippet: file.snippet || '',
- modified: file.modified || '', tokens: file.tokens || [], score: file.score || 0
- });
- }
- tx.oncomplete = () => resolve();
- tx.onerror = () => reject(tx.error);
- });
- },
-
- async getFileIndex(vault, path) {
- const db = await this.open();
- const key = vault + '::' + path;
- return new Promise((resolve) => {
- const tx = db.transaction('files', 'readonly');
- const store = tx.objectStore('files');
- const req = store.get(key);
- req.onsuccess = () => resolve(req.result || null);
- req.onerror = () => resolve(null);
- });
- },
-
- /* Search in file index — title matching and prefix */
- async searchFileIndex(query, vaultFilter) {
- const db = await this.open();
- const lowerQ = query.toLowerCase();
- const words = lowerQ.split(/\s+/).filter(Boolean);
-
- return new Promise((resolve) => {
- const tx = db.transaction('files', 'readonly');
- const store = tx.objectStore('files');
- const results = [];
-
- store.openCursor().onsuccess = (e) => {
- const cursor = e.target.result;
- if (!cursor) {
- // Sort by relevance: title match > path match > snippet match
- results.sort((a, b) => b._score - a._score);
- resolve(results.slice(0, 50));
- return;
- }
-
- const doc = cursor.value;
-
- // Vault filter
- if (vaultFilter && vaultFilter !== 'all' && doc.vault !== vaultFilter) {
- cursor.continue();
- return;
- }
-
- // Score based on match quality
- let score = 0;
- const titleLower = doc.title.toLowerCase();
- const pathLower = doc.path.toLowerCase();
- const snippetLower = (doc.snippet || '').toLowerCase();
-
- for (const word of words) {
- if (titleLower.includes(word)) score += 10;
- else if (pathLower.includes(word)) score += 5;
- else if (snippetLower.includes(word)) score += 2;
- }
-
- // Full query match bonus
- if (titleLower.includes(lowerQ)) score += 5;
- if (pathLower === lowerQ) score += 8;
-
- if (score > 0) {
- results.push({
- vault: doc.vault,
- path: doc.path,
- title: doc.title,
- tags: doc.tags,
- snippet: doc.snippet,
- score: score,
- modified: doc.modified,
- _score: score
- });
- }
-
- cursor.continue();
- };
- });
- },
-
- async getAllFileIndex(vaultFilter) {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('files', 'readonly');
- const store = tx.objectStore('files');
- const results = [];
- store.openCursor().onsuccess = (e) => {
- const cursor = e.target.result;
- if (!cursor) { resolve(results); return; }
- if (!vaultFilter || vaultFilter === 'all' || cursor.value.vault === vaultFilter) {
- results.push(cursor.value);
- }
- cursor.continue();
- };
- });
- },
-
- async clearFileIndex(vaultFilter) {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('files', 'readwrite');
- const store = tx.objectStore('files');
- if (!vaultFilter || vaultFilter === 'all') {
- store.clear().onsuccess = () => resolve();
- } else {
- store.openCursor().onsuccess = (e) => {
- const cursor = e.target.result;
- if (!cursor) { resolve(); return; }
- if (cursor.value.vault === vaultFilter) cursor.delete();
- cursor.continue();
- };
- }
- });
- },
-
- /* ── File Contents ── */
-
- async putFileContent(vault, path, content, modified) {
- const db = await this.open();
- return new Promise((resolve, reject) => {
- const tx = db.transaction('content', 'readwrite');
- const store = tx.objectStore('content');
- const key = vault + '::' + path;
- store.put({ key: key, content: content, modified: modified || '' });
- tx.oncomplete = () => resolve();
- tx.onerror = () => reject(tx.error);
- });
- },
-
- async getFileContent(vault, path) {
- const db = await this.open();
- const key = vault + '::' + path;
- return new Promise((resolve) => {
- const tx = db.transaction('content', 'readonly');
- const store = tx.objectStore('content');
- const req = store.get(key);
- req.onsuccess = () => resolve(req.result || null);
- req.onerror = () => resolve(null);
- });
- },
-
- async removeFileContent(vault, path) {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('content', 'readwrite');
- const store = tx.objectStore('content');
- const key = vault + '::' + path;
- store.delete(key);
- tx.oncomplete = () => resolve();
- });
- },
-
- /* ── Pending Changes Queue ── */
-
- async addPending(entry) {
- const db = await this.open();
- return new Promise((resolve, reject) => {
- const tx = db.transaction('pending', 'readwrite');
- const store = tx.objectStore('pending');
- const doc = {
- vault: entry.vault,
- path: entry.path,
- content: entry.content || '',
- action: entry.action || 'save',
- timestamp: Date.now()
- };
- const req = store.add(doc);
- req.onsuccess = () => resolve(req.result);
- tx.onerror = () => reject(tx.error);
- });
- },
-
- async getPendingCount() {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('pending', 'readonly');
- const store = tx.objectStore('pending');
- store.count().onsuccess = (e) => resolve(e.target.result);
- });
- },
-
- async getAllPending() {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('pending', 'readonly');
- const store = tx.objectStore('pending');
- const results = [];
- const index = store.index('timestamp');
- index.openCursor().onsuccess = (e) => {
- const cursor = e.target.result;
- if (!cursor) { resolve(results); return; }
- results.push(cursor.value);
- cursor.continue();
- };
- });
- },
-
- async clearPending() {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('pending', 'readwrite');
- tx.objectStore('pending').clear().onsuccess = () => resolve();
- });
- },
-
- async removePendingById(id) {
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('pending', 'readwrite');
- tx.objectStore('pending').delete(id);
- tx.oncomplete = () => resolve();
- });
- },
-
- /* ── Stats ── */
-
- async getStats() {
- const db = await this.open();
- const fileCount = await new Promise(r => {
- db.transaction('files','readonly').objectStore('files').count().onsuccess = e => r(e.target.result);
- });
- const contentCount = await new Promise(r => {
- db.transaction('content','readonly').objectStore('content').count().onsuccess = e => r(e.target.result);
- });
- const pendingCount = await this.getPendingCount();
- return { fileCount, contentCount, pendingCount };
- },
-
- /* ── Full Sync ── */
-
- async syncFileIndex(serverFiles) {
- // serverFiles: array of {vault,path,title,tags,snippet,modified,tokens}
- await this.clearFileIndex(null);
- await this.putFileIndexBatch(serverFiles);
- console.log('[OfflineDB] File index synced:', serverFiles.length, 'files');
- },
-
- async pruneContentCache(maxEntries) {
- maxEntries = maxEntries || 50;
- const db = await this.open();
- return new Promise((resolve) => {
- const tx = db.transaction('content', 'readwrite');
- const store = tx.objectStore('content');
- const keys = [];
- store.openCursor().onsuccess = (e) => {
- const cursor = e.target.result;
- if (!cursor) {
- // Delete oldest if over maxEntries
- if (keys.length > maxEntries) {
- const toDelete = keys.slice(0, keys.length - maxEntries);
- for (const k of toDelete) store.delete(k);
- }
- resolve();
- return;
- }
- keys.push(cursor.value.key);
- cursor.continue();
- };
- });
- }
-};
-
-/* ── Network Status ── */
-let _online = navigator.onLine;
-let _listeners = [];
-
-export const NetworkStatus = {
- get isOnline() { return _online; },
-
- init() {
- window.addEventListener('online', () => {
- _online = true;
- _listeners.forEach(fn => fn(true));
- console.log('[Network] Online');
- });
- window.addEventListener('offline', () => {
- _online = false;
- _listeners.forEach(fn => fn(false));
- console.log('[Network] Offline');
- });
- },
-
- onChange(fn) {
- _listeners.push(fn);
- return () => { _listeners = _listeners.filter(f => f !== fn); };
- }
-};
diff --git a/desktop/frontend-static/js/offline.js b/desktop/frontend-static/js/offline.js
deleted file mode 100644
index dddfac5..0000000
--- a/desktop/frontend-static/js/offline.js
+++ /dev/null
@@ -1,209 +0,0 @@
-/* ObsiGate — Offline Integration Module
- * Ties together: IndexedDB, offline search, sync queue, UI indicator
- */
-import { OfflineDB, NetworkStatus } from './offline-db.js';
-import { state } from './state.js';
-import { showToast } from './ui.js';
-
-/* ── Init ── */
-export async function initOffline() {
- NetworkStatus.init();
-
- // Open DB early
- try {
- await OfflineDB.open();
- console.log('[Offline] IndexedDB ready');
- } catch (e) {
- console.warn('[Offline] IndexedDB not available:', e);
- }
-
- NetworkStatus.onChange((online) => {
- updateOfflineIndicator();
- if (online) {
- syncPendingChanges();
- showToast(t('offline.reconnected'), 'info');
- } else {
- showToast(t('offline.mode_offline'), 'warning');
- }
- });
-
- updateOfflineIndicator();
-}
-
-/* ── UI Indicator ── */
-function updateOfflineIndicator() {
- const badge = document.getElementById('offline-badge');
- if (!badge) return;
-
- if (!NetworkStatus.isOnline) {
- // Build or show indicator
- badge.innerHTML = replaceLucideIcon(badge, 'wifi-off',
- ' ');
- badge.title = 'Mode hors-ligne';
- badge.style.display = 'flex';
- badge.className = 'header-icon offline-indicator offline';
-
- // Add pending count if any
- OfflineDB.getPendingCount().then(count => {
- if (count > 0) {
- const span = badge.querySelector('.offline-pending') || document.createElement('span');
- span.className = 'offline-pending';
- span.textContent = String(count);
- if (!badge.contains(span)) badge.appendChild(span);
- }
- });
- } else {
- badge.style.display = 'none';
- badge.className = 'header-icon offline-indicator';
- }
-}
-
-function replaceLucideIcon(container, iconName, html) {
- const existing = container.querySelector('i[data-lucide]');
- if (existing && existing.getAttribute('data-lucide') === iconName) return container.innerHTML;
- return html;
-}
-
-/* ── Sync File Index from Server ── */
-export async function syncFileIndexFromServer() {
- if (!NetworkStatus.isOnline) {
- console.log('[Offline] Skipping index sync — offline');
- return;
- }
- try {
- // Use a wildcard query to get recent files and populate offline index
- const resp = await fetch('/api/search/advanced?q=**&vault=all&limit=200');
- if (!resp.ok) {
- console.warn('[Offline] Index sync failed — API returned', resp.status);
- return;
- }
- const data = await resp.json();
- const files = (data.results || []).map(r => ({
- vault: r.vault,
- path: r.path,
- title: r.title,
- tags: r.tags || [],
- snippet: r.snippet || '',
- modified: r.modified || '',
- tokens: []
- }));
- if (files.length > 0) {
- await OfflineDB.syncFileIndex(files);
- console.log('[Offline] Index synced:', files.length, 'files');
- }
- } catch (e) {
- console.warn('[Offline] Index sync failed:', e.message);
- }
-}
-
-/* ── Cache File When Viewed ── */
-export async function cacheViewedFile(vault, path, content, modified) {
- if (!vault || !path) return;
- try {
- await OfflineDB.putFileContent(vault, path, content, modified);
- await OfflineDB.pruneContentCache(50);
- } catch (e) {
- console.warn('[Offline] Cache file failed:', e);
- }
-}
-
-/* ── Offline Search ── */
-export async function searchOffline(query, vaultFilter) {
- try {
- return await OfflineDB.searchFileIndex(query, vaultFilter || state.currentVault);
- } catch (e) {
- console.error('[Offline] Search failed:', e);
- return [];
- }
-}
-
-/* ── Get Cached File ── */
-export async function getCachedFile(vault, path) {
- try {
- return await OfflineDB.getFileContent(vault, path);
- } catch (e) {
- return null;
- }
-}
-
-/* ── Pending Changes Queue ── */
-let _syncing = false;
-
-export async function queueOfflineSave(vault, path, content) {
- try {
- const id = await OfflineDB.addPending({ vault, path, content, action: 'save' });
- updateOfflineIndicator();
- console.log('[Offline] Queued save:', vault, path, 'id=', id);
- return id;
- } catch (e) {
- console.error('[Offline] Queue save failed:', e);
- return null;
- }
-}
-
-export async function syncPendingChanges() {
- if (!NetworkStatus.isOnline || _syncing) return;
- _syncing = true;
-
- try {
- const pending = await OfflineDB.getAllPending();
- if (pending.length === 0) {
- _syncing = false;
- return;
- }
-
- console.log('[Offline] Syncing', pending.length, 'pending changes...');
-
- for (const entry of pending) {
- try {
- const resp = await fetch('/api/file/' + encodeURIComponent(entry.vault) + '/write', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path: entry.path, content: entry.content })
- });
-
- if (resp.ok) {
- await OfflineDB.removePendingById(entry.id);
- console.log('[Offline] Synced:', entry.vault, entry.path);
- } else if (resp.status === 409) {
- // Conflict — keep in queue for manual resolution
- console.warn('[Offline] Conflict on:', entry.vault, entry.path);
- } else {
- console.error('[Offline] Sync failed for:', entry.vault, entry.path, resp.status);
- }
- } catch (e) {
- console.error('[Offline] Sync error for:', entry.vault, entry.path, e);
- break; // Stop on network error
- }
- }
-
- const remaining = await OfflineDB.getPendingCount();
- console.log('[Offline] Sync complete. Remaining:', remaining);
- updateOfflineIndicator();
-
- if (remaining === 0) {
- showToast(t('offline.all_synced'), 'success');
- } else {
- showToast(remaining + ' modification(s) en attente de resolution', 'warning');
- }
- } finally {
- _syncing = false;
- }
-}
-
-/* ── Conflict Detection ── */
-export async function checkConflict(vault, path, localModified) {
- if (!NetworkStatus.isOnline) return null;
-
- try {
- const resp = await fetch('/api/file/' + encodeURIComponent(vault) + '/info?path=' + encodeURIComponent(path));
- if (!resp.ok) return null;
- const data = await resp.json();
- if (data.modified && data.modified !== localModified) {
- return { serverModified: data.modified, localModified };
- }
- return null;
- } catch {
- return null;
- }
-}
diff --git a/desktop/frontend-static/js/package.json b/desktop/frontend-static/js/package.json
deleted file mode 100644
index 6990891..0000000
--- a/desktop/frontend-static/js/package.json
+++ /dev/null
@@ -1 +0,0 @@
-{"type": "module"}
diff --git a/desktop/frontend-static/js/palette.js b/desktop/frontend-static/js/palette.js
deleted file mode 100644
index 177e3e3..0000000
--- a/desktop/frontend-static/js/palette.js
+++ /dev/null
@@ -1,590 +0,0 @@
-/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */
-import { api } from './auth.js';
-import { showToast, toggleTheme, TabManager } from './ui.js';
-import { openEditor } from './utils.js';
-import { state } from './state.js';
-import { t } from './i18n.js';
-
-function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); }
-
-function getCurrentFile() {
- const id = TabManager._activeTabId;
- if (id) { const t = TabManager._tabs.find(x => x.id === id); if (t) return { vault: t.vault, path: t.path }; }
- if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath };
- return null;
-}
-
-// ── Query parser: +word = required, -word = exclude, word = required ──
-function parseQuery(raw) {
- const positive = [];
- const negative = [];
- const parts = (raw || '').split(/\s+/).filter(Boolean);
- for (const p of parts) {
- if (p.startsWith('-') && p.length > 1) negative.push(p.slice(1).toLowerCase());
- else if (p.startsWith('+')) positive.push(p.slice(1).toLowerCase());
- else positive.push(p.toLowerCase());
- }
- return { positive, negative };
-}
-
-// ── DOM / state ──
-let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
-let _browse = { action: null, vault: null, path: '' };
-let _browseFilterEl = null, _browseFilter = '', _allItems = [];
-let _browseFilterTimer = null; // debounce for global search
-
-// ── Commands ── (lazy: built on first access so i18n is ready)
-let _cmdsCache = null;
-function getCMDS() {
- if (_cmdsCache) return _cmdsCache;
- _cmdsCache = [
- { id:'home', label:t('palette.cmd_back_home'), desc:t('palette.cmd_back_home_desc'), cat:'Navigation', act:()=>{close();triggerGoHome();} },
- { id:'edit-current', label:'✏️ '+t('editor.edit'), desc:t('editor.edit_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} },
- { id:'popout', label:'🪟 Pop-out', desc:t('palette.popout_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}?theme=${document.documentElement.getAttribute('data-theme')||'dark'}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} },
- { id:'create-file', label:t('palette.cmd_create_file'), desc:t('palette.cmd_create_file_desc'), cat:'Fichiers', act:()=>startBrowse('create-file') },
- { id:'delete-file', label:'🗑️ '+t('file.delete'), desc:t('palette.delete_file_desc'), cat:'Fichiers', act:()=>startBrowse('delete-file') },
- { id:'delete-cur', label:'🗑️ '+t('palette.delete_current'), desc:t('palette.delete_current_desc'), cat:'Fichiers', act:async()=>{close();await delCurFile();} },
- { id:'close-cur', label:'✕ '+t('palette.close_current'), desc:t('palette.close_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} },
- { id:'create-dir', label:t('palette.cmd_create_dir'), desc:t('palette.cmd_create_dir_desc'), cat:'Répertoires', act:()=>startBrowse('create-dir') },
- { id:'delete-dir', label:t('palette.cmd_delete_dir'), desc:t('palette.cmd_delete_dir_desc'), cat:'Répertoires', act:()=>startBrowse('delete-dir') },
- { id:'theme', label:t('palette.cmd_theme'), desc:t('palette.cmd_theme_desc'), cat:'Actions', act:()=>{close();toggleTheme();} },
- { id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} },
- { id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
- { id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
- ];
- return _cmdsCache;
-}
-
-// ── Browse: vault picker ──
-function startBrowse(action) {
- _browse = { action, vault: null, path: '' };
- _browseFilter = ''; _allItems = [];
- switchMode('browse-vault');
- renderVaultPicker();
-}
-
-async function renderVaultPicker() {
- try {
- const data = await api('/api/vaults');
- const vaults = Array.isArray(data) ? data : [];
- _items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: t('palette.vault_count', {count: v.file_count}) }));
- if (_items.length === 0) {
- _items = [{ type:'_empty', label: t('palette.no_vaults'), sublabel: '' }];
- }
- _allItems = _items;
- render();
- } catch { _items = [{ type:'_error', label: t('palette.error_loading'), sublabel: '' }]; _allItems = _items; render(); }
-}
-
-// ── Browse: directory navigation ──
-async function renderBrowse() {
- const { vault, path } = _browse;
- _items = [];
- const bc = document.getElementById('cp-breadcrumb');
- if (bc) {
- bc.style.display = 'flex';
- const segs = path ? path.split('/') : [];
- let html = `${esc(vault)} `;
- let acc = '';
- for (const s of segs) { acc = acc ? acc+'/'+s : s; html += `/ ${esc(s)} `; }
- bc.innerHTML = html;
- bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }));
- bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); });
- }
-
- try {
- const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`);
- const items = data.items || [];
- const dirs = items.filter(i => i.type === 'directory' || i.is_dir);
- const files = items.filter(i => i.type === 'file' && !i.is_dir);
-
- // Action row
- const actLabel = {
- 'create-file': t('palette.cmd_create_file'),
- 'create-dir': t('palette.cmd_create_dir'),
- 'delete-dir': t('palette.cmd_delete_dir'),
- 'delete-file': t('palette.delete_file_desc'),
- 'open-file': t('palette.browse_open_file')
- };
- const isDeleteDir = _browse.action === 'delete-dir' && path;
- const isDeleteFile = _browse.action === 'delete-file';
- const isOpenFile = _browse.action === 'open-file';
- const needsName = _browse.action === 'create-file' || _browse.action === 'create-dir';
-
- if (isDeleteDir) {
- _items.push({ type:'_action', label: '🗑️ '+t('file.delete')+' '+(path.split('/').pop()||path), sublabel: t('palette.dir_delete_confirm'), act:()=>execDelDir(path) });
- } else if (needsName) {
- _items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: t('palette.browse_in', {path: path||t('palette.browse_root')}), act:()=>askName() });
- }
-
- // Parent
- if (path) {
- _items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') });
- }
-
- // Subdirs
- for (const d of dirs) {
- const fp = path ? path+'/'+d.name : d.name;
- _items.push({ type:'_nav', label: '📁 '+d.name, go: fp });
- }
-
- // Files
- for (const f of files) {
- const fp = path ? path+'/'+f.name : f.name;
- if (isDeleteFile) {
- _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>execDelFile(fp) });
- } else if (isOpenFile) {
- _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(vault, fp); } });
- } else {
- _items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp });
- }
- }
-
- if (_items.length === 0) _items.push({ type:'_empty', label: t('palette.empty_dir'), sublabel: '' });
- } catch { _items = [{ type:'_error', label: t('common.error'), sublabel: '' }]; }
-
- _allItems = _items;
- applyBrowseFilter();
-}
-
-// ── Browse filter: local when empty, global suggest when text ──
-function applyBrowseFilter() {
- const q = _browseFilter.trim();
- if (!q) {
- // Empty filter: show local directory items
- _items = _allItems;
- render();
- return;
- }
- // Has filter text: debounce and search globally
- if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
- _browseFilterTimer = setTimeout(() => searchBrowseGlobal(q), 200);
-}
-
-async function searchBrowseGlobal(rawQ) {
- const { positive, negative } = parseQuery(rawQ);
- const action = _browse.action;
- const isDeleteFile = action === 'delete-file';
- const isOpenFile = action === 'open-file';
- const isDeleteDir = action === 'delete-dir';
- const needsName = action === 'create-file' || action === 'create-dir';
-
- try {
- const q = positive.join(' ');
- const vault = _browse.vault || 'all';
- const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=${encodeURIComponent(vault)}`);
- const sug = (d.suggestions || []).filter(s => {
- const fullPath = (s.vault+'/'+s.path).toLowerCase();
- // Client-side negative filter
- for (const n of negative) {
- if (fullPath.includes(n)) return false;
- }
- return true;
- });
-
- if (sug.length === 0) {
- _items = [{ type:'_empty', label: t('palette.no_results', {query: rawQ}), sublabel: '' }];
- render();
- return;
- }
-
- _items = [];
-
- // Extract unique directories from result paths
- const dirSet = new Set();
- for (const s of sug) {
- const parts = s.path.split('/');
- // Build parent directories
- for (let i = 0; i < parts.length - 1; i++) {
- dirSet.add(s.vault + '|' + parts.slice(0, i + 1).join('/'));
- }
- }
-
- // Show directories as navigable
- const dirs = [...dirSet].map(d => {
- const [v, p] = d.split('|');
- const name = p.split('/').pop();
- return { type:'_nav', label:'📁 '+name, sublabel:v+'/'+p, go:p, vault:v };
- }).sort((a,b) => a.sublabel.localeCompare(b.sublabel));
-
- for (const d of dirs) _items.push(d);
-
- // Show files
- for (const s of sug) {
- const fp = s.path;
- const name = fp.split('/').pop();
- if (isDeleteFile) {
- _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelFileBrowsed(s.vault, fp) });
- } else if (isOpenFile) {
- _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
- } else if (isDeleteDir) {
- _items.push({ type:'_file', label:'📁 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelDirBrowsed(s.vault, fp) });
- } else {
- _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
- }
- }
-
- render();
- } catch { _items = [{ type:'_error', label: t('palette.search_error'), sublabel: '' }]; render(); }
-}
-
-// Navigate to a directory from global search
-function execNavGlobal(vault, goPath) {
- _browse.vault = vault;
- _browse.path = goPath;
- _browseFilter = '';
- if (_browseFilterEl) _browseFilterEl.value = '';
- renderBrowse();
-}
-
-async function execDelFileBrowsed(vault, path) {
- if (!confirm(t("confirm.delete_file_named", {path: vault + "/" + path}))) return;
- try {
- const d = await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
- if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-async function execDelDirBrowsed(vault, path) {
- if (!confirm(t("confirm.delete_dir_named", {path: path}))) return;
- try {
- const d = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
- if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-function onBrowseFilterInput() {
- _browseFilter = _browseFilterEl ? _browseFilterEl.value : '';
- applyBrowseFilter();
-}
-
-function onBrowseFilterKey(e) {
- if (e.key === 'Escape') {
- e.preventDefault();
- if (_browseFilter) {
- _browseFilter = '';
- if (_browseFilterEl) _browseFilterEl.value = '';
- applyBrowseFilter();
- } else {
- close();
- }
- } else if (e.key === 'ArrowDown') {
- e.preventDefault();
- _sel = 0;
- updSel(_results.querySelectorAll('.cp-item'));
- _overlay.focus();
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- const items = _results.querySelectorAll('.cp-item');
- _sel = Math.max(items.length - 1, 0);
- updSel(items);
- _overlay.focus();
- } else if (e.key === 'Enter') {
- e.preventDefault();
- if (_items.length > 0) exec(_sel);
- } else if (e.key === 'Backspace' && !_browseFilter && (!_browseFilterEl || !_browseFilterEl.value)) {
- e.preventDefault();
- const segs = _browse.path ? _browse.path.split('/') : [];
- if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); }
- else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
- }
-}
-
-// ── Name input modal ──
-function askName() {
- const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier';
- const ext = _browse.action === 'create-file' ? '(.md ajouté si besoin)' : '';
- const existingInput = document.getElementById('cp-name-input');
- if (existingInput) { existingInput.focus(); return; }
-
- _input.style.display = 'none';
- const bc = document.getElementById('cp-breadcrumb');
- if (bc) bc.style.display = 'none';
-
- const nameDiv = document.createElement('div');
- nameDiv.id = 'cp-name-input';
- nameDiv.className = 'cp-name-input';
- nameDiv.innerHTML = `
- Enter confirmer · Esc annuler `;
- _results.before(nameDiv);
-
- const field = document.getElementById('cp-name-field');
- field.focus();
- field.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- const name = field.value.trim();
- if (!name) return;
- cleanupNameInput();
- if (_browse.action === 'create-file') execCreateFile(name);
- else execCreateDir(name);
- } else if (e.key === 'Escape') {
- e.preventDefault();
- cleanupNameInput();
- _overlay.focus();
- renderBrowse();
- }
- });
-}
-
-function cleanupNameInput() {
- const el = document.getElementById('cp-name-input');
- if (el) el.remove();
- const bc = document.getElementById('cp-breadcrumb');
- if (bc) bc.style.display = 'flex';
-}
-
-// ── Execute actions ──
-async function execCreateFile(name) {
- let fp = _browse.path ? _browse.path+'/'+name : name;
- if (!fp.includes('.')) fp += '.md';
- try {
- const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:fp, content:`# ${name.replace('.md','')}\n\n`}) });
- if (d?.status==='ok' || d?.success) { showToast(`✅ ${fp} créé`,'success'); close(); openEditor(_browse.vault, fp); }
- else showToast(t('toast.create_error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-async function execCreateDir(name) {
- const dp = _browse.path ? _browse.path+'/'+name : name;
- try {
- const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:dp}) });
- if (d?.status==='ok' || d?.success) { showToast(`📁 ${dp} créé`,'success'); close(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-async function execDelDir(path) {
- if (!confirm(t("confirm.delete_dir_named", {path: path}))) return;
- try {
- const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
- if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-async function execDelFile(path) {
- if (!confirm(t("confirm.delete_file_named", {path: path}))) return;
- try {
- const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
- if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-async function delCurFile() {
- const f = getCurrentFile();
- if (!f) { showToast(t('toast.no_open_file'),'error'); return; }
- if (!confirm(t("confirm.delete_file_named", {path: f.path}))) return;
- try {
- const d = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method:'DELETE' });
- if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${f.path} supprimé`,'success');
- const tab = TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path); if(tab) TabManager.close(tab.id); triggerGoHome(); refreshSidebar(); }
- else showToast(t('common.error'),'error');
- } catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
-}
-
-function refreshSidebar() {
- import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {});
-}
-
-// ── DOM ──
-function createDOM() {
- if (_overlay) return;
- _overlay = document.createElement('div');
- _overlay.className = 'command-palette-overlay';
- _overlay.tabIndex = -1;
- _overlay.innerHTML = ``;
- _input = _overlay.querySelector('.cp-input');
- _browseFilterEl = _overlay.querySelector('#cp-browse-filter');
- _results = _overlay.querySelector('.cp-results');
- _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); });
- _input.addEventListener('keydown', onKeyDown);
- _input.addEventListener('input', onInput);
- if (_browseFilterEl) {
- _browseFilterEl.addEventListener('input', onBrowseFilterInput);
- _browseFilterEl.addEventListener('keydown', onBrowseFilterKey);
- }
- _overlay.addEventListener('keydown', onOverlayKeyDown);
- document.body.appendChild(_overlay);
-}
-
-export function open(q = '') {
- createDOM();
- _overlay.classList.add('active');
- _input.value = q;
- _mode = q.startsWith('>') ? 'commands' : 'files';
- _input.disabled = false; _input.style.display = '';
- _sel = 0; _items = []; _results.innerHTML = '';
- _input.focus();
- onInput();
-}
-
-export function close() {
- if (!_overlay) return;
- _overlay.classList.remove('active');
- if (_to) clearTimeout(_to); _to = null;
- if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
- _mode = 'files';
- _browseFilter = '';
- _allItems = [];
- cleanupNameInput();
- if (_browseFilterEl) { _browseFilterEl.style.display = 'none'; _browseFilterEl.value = ''; }
- const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
- const si = document.querySelector('.search-input'); if (si) si.focus();
-}
-
-function isOpen() { return _overlay && _overlay.classList.contains('active'); }
-
-function switchMode(m) {
- _mode = m;
- if (m === 'browse' || m === 'browse-vault') {
- _input.style.display = 'none'; _input.disabled = true;
- _overlay.focus();
- if (m === 'browse' && _browseFilterEl) {
- _browseFilterEl.style.display = '';
- _browseFilterEl.value = _browseFilter;
- _browseFilterEl.focus();
- } else if (_browseFilterEl) {
- _browseFilterEl.style.display = 'none';
- }
- } else {
- _input.style.display = ''; _input.disabled = false;
- _input.focus();
- if (_browseFilterEl) _browseFilterEl.style.display = 'none';
- const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
- }
-}
-
-function onInput() {
- if (_mode === 'browse' || _mode === 'browse-vault') return;
- const v = _input.value;
- const nm = v.startsWith('>') ? 'commands' : 'files';
- if (nm !== _mode) { _mode = nm; _input.placeholder = _mode==='commands'?'Tapez une commande...':'Rechercher un fichier... (+inclure -exclure)'; }
- if (_to) clearTimeout(_to);
- _to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
-}
-
-async function searchFiles(rawQ) {
- if (!rawQ) {
- _items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }];
- render();
- return;
- }
-
- const { positive, negative } = parseQuery(rawQ);
-
- try {
- const q = positive.join(' ');
- const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`);
- let sug = d.suggestions || [];
-
- // Client-side negative filter
- if (negative.length > 0) {
- sug = sug.filter(s => {
- const fullPath = (s.vault+'/'+s.path).toLowerCase();
- for (const n of negative) {
- if (fullPath.includes(n)) return false;
- }
- return true;
- });
- }
-
- if (sug.length === 0) {
- _items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }];
- } else {
- _items = sug.map(s => ({ type:'file', vault:s.vault, path:s.path, label:s.title||s.path.split('/').pop(), sublabel:`${s.vault}/${s.path}` }));
- }
- render();
- } catch { _results.innerHTML = 'Erreur
'; }
-}
-
-function searchCmd(q) {
- if (!q) { _items = getCMDS().map(c => ({ type:'cmd', ...c })); render(); return; }
- const lq = q.toLowerCase();
- _items = getCMDS().filter(c => c.label.toLowerCase().includes(lq)||c.desc.toLowerCase().includes(lq)||(c.cat&&c.cat.toLowerCase().includes(lq))).map(c => ({ type:'cmd', ...c }));
- render();
-}
-
-function render() {
- _sel = 0;
- if (!_items.length) { _results.innerHTML = 'Aucun résultat
'; return; }
- _results.innerHTML = _items.map((it, i) => `${esc(it.label)}
${esc(it.sublabel||'')}
`).join('');
- _results.querySelectorAll('.cp-item').forEach(el => {
- el.addEventListener('click', () => exec(parseInt(el.dataset.idx)));
- el.addEventListener('mouseenter', () => { document.querySelectorAll('.cp-item').forEach(e=>e.classList.remove('cp-selected')); el.classList.add('cp-selected'); _sel = parseInt(el.dataset.idx); });
- });
- scroll();
-}
-
-function onKeyDown(e) {
- const items = _results.querySelectorAll('.cp-item');
- if (_mode === 'browse' || _mode === 'browse-vault') {
- if (e.key === 'Escape') { e.preventDefault(); close(); }
- else if (e.key === 'Backspace') {
- e.preventDefault();
- if (_mode === 'browse-vault') { _mode='commands'; _input.style.display=''; _input.disabled=false; _input.focus(); searchCmd(''); return; }
- const segs = _browse.path ? _browse.path.split('/') : [];
- if (segs.length > 0) { _browse.path = segs.slice(0,-1).join('/'); renderBrowse(); }
- else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
- }
- else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
- else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
- else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
- return;
- }
- if (e.key === 'Escape') { e.preventDefault(); close(); }
- else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
- else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
- else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
- else if (e.key === 'Tab') { e.preventDefault(); if (_mode==='files') { _input.value='> '; setTimeout(()=>_input.setSelectionRange(2,2),0); } else _input.value=''; onInput(); }
-}
-
-function updSel(items) { items.forEach((el,i)=>el.classList.toggle('cp-selected',i===_sel)); scroll(); }
-function scroll() { const s = _results.querySelector('.cp-selected'); if (s) s.scrollIntoView({block:'nearest'}); }
-
-// ── Overlay keydown handler (for browse modes where input is hidden) ──
-function onOverlayKeyDown(e) {
- if (_mode !== 'browse' && _mode !== 'browse-vault') return;
- if (document.getElementById('cp-name-input')) return;
- if (document.activeElement === _browseFilterEl) return;
- onKeyDown(e);
-}
-
-function exec(idx) {
- const it = _items[idx]; if (!it) return;
- if (it.type === 'file') {
- close();
- if (window.innerWidth <= 768) {
- TabManager.openPersistent(it.vault, it.path);
- } else {
- TabManager.openPreview(it.vault, it.path);
- }
- }
- else if (it.type === 'cmd' || it.type === '_browse') { it.act(); }
- else if (it.type === '_vault') { _browse.vault = it.vault; switchMode('browse'); renderBrowse(); }
- else if (it.type === '_nav') {
- // Check if this is a global search result with vault info
- if (it.vault && it.vault !== _browse.vault) { _browse.vault = it.vault; }
- _browse.path = it.go;
- _browseFilter='';
- if(_browseFilterEl) _browseFilterEl.value='';
- renderBrowse();
- }
- else if (it.type === '_action' || it.type === '_file') { it.act(); }
-}
-
-function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"'); }
-
-export function initCommandPalette() {
- document.addEventListener('keydown', (e) => {
- if ((e.ctrlKey||e.metaKey) && e.shiftKey && !e.altKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open(''); }
- if ((e.ctrlKey||e.metaKey) && e.altKey && !e.shiftKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open('> '); }
- });
-}
diff --git a/desktop/frontend-static/js/pane-manager.js b/desktop/frontend-static/js/pane-manager.js
deleted file mode 100644
index 2e9324d..0000000
--- a/desktop/frontend-static/js/pane-manager.js
+++ /dev/null
@@ -1,263 +0,0 @@
-/**
- * PaneManager — Multi-pane split view for ObsiGate (v2.0.0-dev)
- * Supports 1–4 panes in CSS grid, split right/down, resize, drag tabs between panes.
- * Integrates with existing TabManager singleton.
- */
-
-const PANE_MAX = 4;
-
-const PaneManager = {
- panes: [],
- activePaneId: 0,
- _paneCounter: 1,
- layout: 'horizontal',
-
- init() {
- const saved = localStorage.getItem('obsigate-panes');
- if (saved) {
- try {
- const data = JSON.parse(saved);
- if (data.panes && data.panes.length >= 2) {
- this.layout = data.layout || 'horizontal';
- this._paneCounter = data.panes.length;
- this._buildGrid(data.panes.length);
- data.panes.forEach((p, i) => {
- this.panes[i].activeTab = p.activeTab;
- this.panes[i].width = p.width;
- });
- this.activePaneId = data.panes[0].id;
- this._applyResize();
- return;
- }
- } catch (e) { /* ignore */ }
- }
- },
-
- _buildGrid(n) {
- const wrapper = document.querySelector('.content-wrapper');
- const origTabBar = document.getElementById('tab-bar');
- const origContent = document.getElementById('content-area');
- if (!wrapper || !origContent || n < 2) return;
-
- // Create grid container
- const grid = document.createElement('div');
- grid.className = 'pane-grid';
- if (n === 2) grid.style.gridTemplateColumns = this.layout === 'horizontal' ? '1fr 4px 1fr' : '1fr';
- if (n >= 3) grid.style.gridTemplateColumns = '1fr 4px 1fr';
- if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr';
-
- wrapper.innerHTML = '';
- wrapper.appendChild(grid);
-
- for (let i = 0; i < Math.min(n, PANE_MAX); i++) {
- const pane = this._createPane(i, grid, i === 0 ? origTabBar : null, i === 0 ? origContent : null);
- this.panes.push(pane);
- if (i < n - 1) {
- const handle = document.createElement('div');
- handle.className = 'pane-resize-handle';
- if (this.layout === 'horizontal') handle.style.cursor = 'col-resize';
- else handle.style.cursor = 'row-resize';
- handle.addEventListener('mousedown', (e) => this._onResizeStart(e, i, i + 1));
- handle.addEventListener('dblclick', () => this._resetResize(i, i + 1));
- grid.appendChild(handle);
- }
- }
- this.activePaneId = 0;
- this._highlightActive();
- },
-
- _createPane(idx, grid, reuseTabBar, reuseContent) {
- const container = document.createElement('div');
- container.className = 'pane-container';
- container.dataset.pane = idx;
-
- if (reuseTabBar && reuseContent) {
- // Pane 0 reuses existing DOM
- container.appendChild(reuseTabBar);
- container.appendChild(reuseContent);
- reuseContent.id = 'content-area-0';
- } else {
- const tabBar = document.createElement('div');
- tabBar.className = 'tab-bar pane-tab-bar';
- tabBar.id = 'tab-bar-' + idx;
- tabBar.innerHTML = '
';
- const content = document.createElement('main');
- content.className = 'content-area pane-content';
- content.id = 'content-area-' + idx;
- container.appendChild(tabBar);
- container.appendChild(content);
- }
-
- container.addEventListener('click', () => {
- this.activePaneId = idx;
- this._highlightActive();
- });
-
- grid.appendChild(container);
- return { id: idx, element: container, width: 100 / this.panes.length + '%' };
- },
-
- _highlightActive() {
- this.panes.forEach((p, i) => {
- p.element.classList.toggle('pane-active', i === this.activePaneId);
- });
- },
-
- splitRight() {
- if (this.panes.length >= PANE_MAX) return;
- if (this.panes.length === 0) {
- // First split: promote single pane to grid
- this._buildGrid(2);
- } else {
- this._addPane();
- }
- this._persist();
- },
-
- splitDown() {
- if (this.panes.length >= PANE_MAX) return;
- this.layout = 'vertical';
- if (this.panes.length === 0) {
- this._buildGrid(2);
- } else {
- this._addPane();
- }
- this._persist();
- },
-
- _addPane() {
- const grid = document.querySelector('.pane-grid');
- if (!grid) return;
- const n = this.panes.length + 1;
- const handle = document.createElement('div');
- handle.className = 'pane-resize-handle';
- handle.addEventListener('mousedown', (e) => this._onResizeStart(e, n - 2, n - 1));
- grid.appendChild(handle);
- const pane = this._createPane(n - 1, grid, null, null);
- this.panes.push(pane);
- this.activePaneId = n - 1;
- this._highlightActive();
- if (n <= 2) grid.style.gridTemplateColumns = '1fr 4px 1fr';
- else if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr';
- },
-
- closePane(paneId) {
- if (this.panes.length <= 1) return;
- const idx = this.panes.findIndex(p => p.id === paneId);
- if (idx < 0) return;
- // Remove pane and its resize handles
- this.panes[idx].element.remove();
- const handles = document.querySelectorAll('.pane-resize-handle');
- if (handles[idx]) handles[idx].remove();
- else if (handles[idx - 1]) handles[idx - 1].remove();
- this.panes.splice(idx, 1);
- if (this.panes.length <= 1) {
- this._collapseToSingle();
- } else {
- this.activePaneId = Math.min(this.activePaneId, this.panes.length - 1);
- this._highlightActive();
- }
- this._persist();
- },
-
- _collapseToSingle() {
- const grid = document.querySelector('.pane-grid');
- if (!grid) return;
- const wrapper = grid.parentElement;
- const pane0 = this.panes[0];
- wrapper.innerHTML = '';
- const tabBar = pane0.element.querySelector('.tab-bar') || document.getElementById('tab-bar-0');
- const content = pane0.element.querySelector('.content-area') || document.getElementById('content-area-0');
- if (tabBar) wrapper.appendChild(tabBar);
- if (content) wrapper.appendChild(content);
- this.panes = [];
- this.activePaneId = 0;
- },
-
- _onResizeStart(e, leftIdx, rightIdx) {
- e.preventDefault();
- const grid = document.querySelector('.pane-grid');
- const startX = e.clientX;
- const startY = e.clientY;
- const totalSize = this.layout === 'horizontal' ? grid.offsetWidth : grid.offsetHeight;
-
- const onMove = (ev) => {
- const delta = this.layout === 'horizontal' ? ev.clientX - startX : ev.clientY - startY;
- const pct = (delta / totalSize) * 100;
- const leftEl = this.panes[leftIdx].element;
- const rightEl = this.panes[rightIdx].element;
- const curLeft = parseFloat(leftEl.style.flexBasis || leftEl.style.width || '50');
- const curRight = parseFloat(rightEl.style.flexBasis || rightEl.style.width || '50');
- const newLeft = Math.max(20, Math.min(80, curLeft + pct));
- const newRight = 100 - newLeft;
- leftEl.style.flexBasis = newLeft + '%';
- rightEl.style.flexBasis = newRight + '%';
- this.panes[leftIdx].width = newLeft + '%';
- this.panes[rightIdx].width = newRight + '%';
- };
-
- const onUp = () => {
- document.removeEventListener('mousemove', onMove);
- document.removeEventListener('mouseup', onUp);
- this._persist();
- };
-
- document.addEventListener('mousemove', onMove);
- document.addEventListener('mouseup', onUp);
- },
-
- _resetResize(leftIdx, rightIdx) {
- this.panes[leftIdx].element.style.flexBasis = '50%';
- this.panes[rightIdx].element.style.flexBasis = '50%';
- this._persist();
- },
-
- _applyResize() {
- this.panes.forEach(p => {
- if (p.width) p.element.style.flexBasis = p.width;
- });
- },
-
- _persist() {
- if (this.panes.length <= 1) {
- localStorage.removeItem('obsigate-panes');
- return;
- }
- const data = {
- layout: this.layout,
- panes: this.panes.map(p => ({ id: p.id, activeTab: p.activeTab, width: p.width })),
- };
- localStorage.setItem('obsigate-panes', JSON.stringify(data));
- },
-
- getActiveContentArea() {
- if (this.panes.length < 2) return document.getElementById('content-area');
- return document.getElementById('content-area-' + this.activePaneId) || document.getElementById('content-area-0');
- },
-
- isSplit() {
- return this.panes.length >= 2;
- },
-};
-
-// Export and auto-init
-if (typeof window !== 'undefined') {
- window.PaneManager = PaneManager;
- document.addEventListener('DOMContentLoaded', () => {
- // Register keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') {
- e.preventDefault();
- PaneManager.splitRight();
- }
- if (e.ctrlKey && e.altKey && e.shiftKey && e.key === '\\') {
- e.preventDefault();
- PaneManager.splitDown();
- }
- });
- // Initialize if saved state exists
- setTimeout(() => PaneManager.init(), 500);
- });
-}
-
-export default PaneManager;
diff --git a/desktop/frontend-static/js/search.js b/desktop/frontend-static/js/search.js
deleted file mode 100644
index 69cbc74..0000000
--- a/desktop/frontend-static/js/search.js
+++ /dev/null
@@ -1,1154 +0,0 @@
-/* ObsiGate — Search module */
-import { api } from './auth.js';
-import { state } from './state.js';
-import { safeCreateIcons } from './utils.js';
-import { showLoading, el, hideProgressBar, showWelcome, highlightSearchText, loadSavedSearches } from './viewer.js';
-import { searchOffline } from './offline.js';
-import { _getEffective } from './config.js';
-import { TabManager, showToast } from './ui.js';
-import { addTagFilter, buildSearchResultsHeader, shouldDisplayPath, removeTagFilter, TagFilterService } from './sidebar.js';
-import { t } from './i18n.js';
-// ---------------------------------------------------------------------------
-// Search History Service (localStorage, LIFO, max 50, dedup)
-// ---------------------------------------------------------------------------
-export const SearchHistory = {
- _load() {
- try {
- const raw = localStorage.getItem(state.SEARCH_HISTORY_KEY);
- return raw ? JSON.parse(raw) : [];
- } catch {
- return [];
- }
- },
- _save(entries) {
- try {
- localStorage.setItem(state.SEARCH_HISTORY_KEY, JSON.stringify(entries));
- } catch {}
- },
- getAll() {
- return this._load();
- },
- add(query) {
- if (!query || !query.trim()) return;
- const q = query.trim();
- let entries = this._load().filter((e) => e !== q);
- entries.unshift(q);
- if (entries.length > state.MAX_HISTORY_ENTRIES) entries = entries.slice(0, state.MAX_HISTORY_ENTRIES);
- this._save(entries);
- },
- remove(query) {
- const entries = this._load().filter((e) => e !== query);
- this._save(entries);
- },
- clear() {
- this._save([]);
- },
- filter(prefix) {
- if (!prefix) return this.getAll().slice(0, 8);
- const lp = prefix.toLowerCase();
- return this._load()
- .filter((e) => e.toLowerCase().includes(lp))
- .slice(0, 8);
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:)
-// ---------------------------------------------------------------------------
-export const QueryParser = {
- parse(raw) {
- const result = { tags: [], vault: null, title: null, path: null, ext: null,
- created: null, modified: null, size: null, freeText: "" };
- if (!raw) return result;
- const tokens = this._tokenize(raw);
- const freeTokens = [];
- for (const tok of tokens) {
- const lower = tok.toLowerCase();
- if (lower.startsWith("tag:")) {
- const v = tok.slice(4).replace(/"/g, "").trim().replace(/^#/, "");
- if (v) result.tags.push(v);
- } else if (lower.startsWith("#") && tok.length > 1) {
- result.tags.push(tok.slice(1));
- } else if (lower.startsWith("vault:")) {
- result.vault = tok.slice(6).replace(/"/g, "").trim();
- } else if (lower.startsWith("title:")) {
- result.title = tok.slice(6).replace(/"/g, "").trim();
- } else if (lower.startsWith("path:")) {
- result.path = tok.slice(5).replace(/"/g, "").trim();
- } else if (lower.startsWith("ext:")) {
- result.ext = tok.slice(4).replace(/"/g, "").trim().replace(/^\./, "").toLowerCase();
- } else if (lower.startsWith("created:")) {
- result.created = tok.slice(8).replace(/"/g, "").trim();
- } else if (lower.startsWith("modified:")) {
- result.modified = tok.slice(9).replace(/"/g, "").trim();
- } else if (lower.startsWith("size:")) {
- result.size = tok.slice(5).replace(/"/g, "").trim();
- } else {
- freeTokens.push(tok);
- }
- }
- result.freeText = freeTokens.join(" ");
- return result;
- },
- _tokenize(raw) {
- const tokens = [];
- let i = 0;
- const n = raw.length;
- while (i < n) {
- while (i < n && raw[i] === " ") i++;
- if (i >= n) break;
- if (raw[i] !== '"') {
- let j = i;
- while (j < n && raw[j] !== " ") {
- if (raw[j] === '"') {
- j++;
- while (j < n && raw[j] !== '"') j++;
- if (j < n) j++;
- } else j++;
- }
- tokens.push(raw.slice(i, j).replace(/"/g, ""));
- i = j;
- } else {
- i++;
- let j = i;
- while (j < n && raw[j] !== '"') j++;
- tokens.push(raw.slice(i, j));
- i = j + 1;
- }
- }
- return tokens;
- },
- /** Detect the current operator context at cursor for autocomplete */
- getContext(raw, cursorPos) {
- const before = raw.slice(0, cursorPos);
- // Check if we're typing a tag: or # value
- const tagMatch = before.match(/(?:tag:|#)([\w-]*)$/i);
- if (tagMatch) return { type: "tag", prefix: tagMatch[1] };
- // Check if typing title:
- const titleMatch = before.match(/title:([\w-]*)$/i);
- if (titleMatch) return { type: "title", prefix: titleMatch[1] };
- // Default: free text
- const words = before.trim().split(/\s+/);
- const lastWord = words[words.length - 1] || "";
- return { type: "text", prefix: lastWord };
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Autocomplete Dropdown Controller
-// ---------------------------------------------------------------------------
-export const AutocompleteDropdown = {
- _dropdown: null,
- _historySection: null,
- _titlesSection: null,
- _tagsSection: null,
- _historyList: null,
- _titlesList: null,
- _tagsList: null,
- _emptyEl: null,
- _suggestTimer: null,
-
- init() {
- this._dropdown = document.getElementById("search-dropdown");
- this._historySection = document.getElementById("search-dropdown-history");
- this._titlesSection = document.getElementById("search-dropdown-titles");
- this._tagsSection = document.getElementById("search-dropdown-tags");
- this._historyList = document.getElementById("search-dropdown-history-list");
- this._titlesList = document.getElementById("search-dropdown-titles-list");
- this._tagsList = document.getElementById("search-dropdown-tags-list");
- this._emptyEl = document.getElementById("search-dropdown-empty");
-
- // Clear history button
- const clearBtn = document.getElementById("search-dropdown-clear-history");
- if (clearBtn) {
- clearBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- SearchHistory.clear();
- this.hide();
- });
- }
-
- // Close dropdown on outside click
- document.addEventListener("click", (e) => {
- if (this._dropdown && !this._dropdown.contains(e.target) && e.target.id !== "search-input") {
- this.hide();
- }
- });
- },
-
- show() {
- if (this._dropdown) this._dropdown.hidden = false;
- },
-
- hide() {
- if (this._dropdown) this._dropdown.hidden = true;
- state.dropdownActiveIndex = -1;
- state.dropdownItems = [];
- },
-
- isVisible() {
- return this._dropdown && !this._dropdown.hidden;
- },
-
- /** Populate and show the dropdown with history, title suggestions, and tag suggestions */
- async populate(inputValue, cursorPos) {
- if (this._suppressNext) { this._suppressNext = false; return; }
- // Cancel previous suggestion request
- if (state.suggestAbortController) {
- state.suggestAbortController.abort();
- state.suggestAbortController = null;
- }
-
- const ctx = QueryParser.getContext(inputValue, cursorPos);
- const vault = document.getElementById("vault-filter").value;
-
- // History — always show filtered history
- const historyItems = SearchHistory.filter(inputValue).slice(0, 5);
- this._renderHistory(historyItems, inputValue);
-
- // Title and tag suggestions from API (debounced) — always fetch both
- clearTimeout(this._suggestTimer);
- const prefix = ctx.prefix;
- if (prefix && prefix.length >= 2) {
- // Only show placeholder if lists are empty (avoid flashing on fast typing)
- const hasTitles = this._titlesList.children.length > 0 && !this._titlesList.querySelector(".search-dropdown__item--loading");
- const hasTags = this._tagsList.children.length > 0 && !this._tagsList.querySelector(".search-dropdown__item--loading");
- if (!hasTitles) {
- this._titlesList.innerHTML = 'Recherche... ';
- }
- if (!hasTags) {
- this._tagsList.innerHTML = 'Recherche... ';
- }
- this._titlesSection.hidden = false;
- this._tagsSection.hidden = false;
- this.show();
- this._suggestTimer = setTimeout(() => this._fetchSuggestions(prefix, vault), 150);
- } else {
- this._renderTitles([], "");
- this._renderTags([], "");
- this._titlesSection.hidden = true;
- this._tagsSection.hidden = true;
- }
-
- // Show/hide sections
- this._historySection.hidden = historyItems.length === 0;
- const hasContent = historyItems.length > 0;
- if (hasContent || (prefix && prefix.length >= 2)) {
- this.show();
- } else {
- this.hide();
- }
-
- this._collectItems();
- },
-
- async _fetchSuggestions(prefix, vault) {
- state.suggestAbortController = new AbortController();
- // Fetch titles
- try {
- const titlesRes = await api(`/api/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: state.suggestAbortController.signal });
- this._renderTitles(titlesRes.suggestions || [], prefix);
- this._titlesSection.hidden = !(titlesRes.suggestions || []).length;
- if (titlesRes.suggestions?.length) this.show();
- } catch (err) {
- if (err.name === "AbortError") return;
- this._titlesSection.hidden = true;
- }
- // Fetch tags — keep section always visible to confirm it works
- try {
- const tagsRes = await api(`/api/tags/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: state.suggestAbortController.signal });
- const items = tagsRes.suggestions || [];
- if (items.length > 0) {
- this._renderTags(items, prefix);
- } else {
- this._tagsList.innerHTML = 'Aucun tag ';
- }
- this._tagsSection.hidden = false;
- this.show();
- } catch (err) {
- if (err.name === "AbortError") return;
- this._tagsList.innerHTML = 'Erreur chargement ';
- this._tagsSection.hidden = false;
- }
- this._collectItems();
- },
-
- _renderHistory(items, query) {
- this._historyList.innerHTML = "";
- items.forEach((entry) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--history", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- textEl.textContent = entry;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- input.value = entry;
- this._suppressNext = true;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- this.hide();
- _triggerAdvancedSearch(entry);
- });
- this._historyList.appendChild(li);
- });
- },
-
- _renderTitles(items, prefix) {
- this._titlesList.innerHTML = "";
- items.forEach((item) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--title", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- if (prefix) {
- this._highlightText(textEl, item.title, prefix);
- } else {
- textEl.textContent = item.title;
- }
- const metaEl = el("span", { class: "search-dropdown__meta" });
- metaEl.textContent = item.vault;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.appendChild(metaEl);
- li.addEventListener("click", () => {
- this.hide();
- TabManager.openPreview(item.vault, item.path);
- });
- this._titlesList.appendChild(li);
- });
- },
-
- _renderTags(items, prefix) {
- this._tagsList.innerHTML = "";
- items.forEach((item) => {
- const li = el("li", { class: "search-dropdown__item search-dropdown__item--tag", role: "option" });
- const iconEl = el("span", { class: "search-dropdown__icon" });
- iconEl.innerHTML = ' ';
- const textEl = el("span", { class: "search-dropdown__text" });
- if (prefix) {
- this._highlightText(textEl, item.tag, prefix);
- } else {
- textEl.textContent = item.tag;
- }
- const badge = el("span", { class: "search-dropdown__badge" });
- badge.textContent = item.count;
- li.appendChild(iconEl);
- li.appendChild(textEl);
- li.appendChild(badge);
- li.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- const current = input.value;
- const cursorPos = input.selectionStart;
- const ctx = QueryParser.getContext(current, cursorPos);
- if (ctx.type === "tag") {
- // Replace the partial tag prefix
- const before = current.slice(0, cursorPos - ctx.prefix.length);
- input.value = before + item.tag + " ";
- } else {
- // Replace the last word with tag: operator
- const words = current.trim().split(/\s+/);
- if (words.length > 0 && ctx.prefix && ctx.prefix.length > 0) {
- words[words.length - 1] = ""; // remove last partial word
- }
- const base = words.filter(w => w).join(" ");
- input.value = (base ? base + " " : "") + "tag:" + item.tag + " ";
- }
- this._suppressNext = true;
- input.dispatchEvent(new Event("input", { bubbles: true }));
- this.hide();
- input.focus();
- _triggerAdvancedSearch(input.value);
- });
- this._tagsList.appendChild(li);
- });
- },
-
- _highlightText(container, text, query) {
- const lower = text.toLowerCase();
- const needle = query.toLowerCase();
- const pos = lower.indexOf(needle);
- if (pos === -1) {
- container.textContent = text;
- return;
- }
- container.appendChild(document.createTextNode(text.slice(0, pos)));
- const markEl = el("mark", {}, [document.createTextNode(text.slice(pos, pos + query.length))]);
- container.appendChild(markEl);
- container.appendChild(document.createTextNode(text.slice(pos + query.length)));
- },
-
- _collectItems() {
- state.dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item"));
- state.dropdownActiveIndex = -1;
- state.dropdownItems.forEach((item) => item.classList.remove("active"));
- },
-
- navigateDown() {
- if (!this.isVisible() || state.dropdownItems.length === 0) return;
- if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active");
- state.dropdownActiveIndex = (state.dropdownActiveIndex + 1) % state.dropdownItems.length;
- state.dropdownItems[state.dropdownActiveIndex].classList.add("active");
- state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" });
- },
-
- navigateUp() {
- if (!this.isVisible() || state.dropdownItems.length === 0) return;
- if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active");
- state.dropdownActiveIndex = state.dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : state.dropdownActiveIndex - 1;
- state.dropdownItems[state.dropdownActiveIndex].classList.add("active");
- state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" });
- },
-
- selectActive() {
- if (state.dropdownActiveIndex >= 0 && state.dropdownActiveIndex < state.dropdownItems.length) {
- state.dropdownItems[state.dropdownActiveIndex].click();
- return true;
- }
- return false;
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Search Chips Controller — renders active filter chips from parsed query
-// ---------------------------------------------------------------------------
-export const SearchChips = {
- _container: null,
- init() {
- this._container = document.getElementById("search-chips");
- },
- update(parsed) {
- if (!this._container) return;
- this._container.innerHTML = "";
- let hasChips = false;
- parsed.tags.forEach((tag) => {
- this._addChip("tag", `tag:${tag}`, tag);
- hasChips = true;
- });
- if (parsed.vault) {
- this._addChip("vault", `vault:${parsed.vault}`, parsed.vault);
- hasChips = true;
- }
- if (parsed.title) {
- this._addChip("title", `title:${parsed.title}`, parsed.title);
- hasChips = true;
- }
- if (parsed.path) {
- this._addChip("path", `path:${parsed.path}`, parsed.path);
- hasChips = true;
- }
- if (parsed.ext) {
- this._addChip("ext", `ext:${parsed.ext}`, parsed.ext);
- hasChips = true;
- }
- if (parsed.created) {
- this._addChip("date", `created:${parsed.created}`, `créé ${parsed.created}`);
- hasChips = true;
- }
- if (parsed.modified) {
- this._addChip("date", `modified:${parsed.modified}`, `modifié ${parsed.modified}`);
- hasChips = true;
- }
- if (parsed.size) {
- this._addChip("size", `size:${parsed.size}`, `taille ${parsed.size}`);
- hasChips = true;
- }
- this._container.hidden = !hasChips;
- },
- clear() {
- if (!this._container) return;
- this._container.innerHTML = "";
- this._container.hidden = true;
- },
- _addChip(type, fullOperator, displayText) {
- const chip = el("span", { class: `search-chip search-chip--${type}` });
- const label = el("span", { class: "search-chip__label" });
- label.textContent = fullOperator;
- const removeBtn = el("button", { class: "search-chip__remove", title: "Retirer ce filtre", type: "button" });
- removeBtn.innerHTML = ' ';
- removeBtn.addEventListener("click", () => {
- // Remove this operator from the input
- const input = document.getElementById("search-input");
- const raw = input.value;
- // Remove the operator text from the query
- const escaped = fullOperator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- input.value = raw.replace(new RegExp("\\s*" + escaped + "\\s*", "i"), " ").trim();
- _triggerAdvancedSearch(input.value);
- });
- chip.appendChild(label);
- chip.appendChild(removeBtn);
- this._container.appendChild(chip);
- safeCreateIcons();
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Helper: trigger advanced search from input value
-// ---------------------------------------------------------------------------
-function _triggerAdvancedSearch(rawQuery) {
- const q = (rawQuery || "").trim();
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- state.advancedSearchOffset = 0;
- if (q.length > 0 || tagFilter) {
- SearchHistory.add(q);
- performAdvancedSearch(q, vault, tagFilter);
- } else {
- SearchChips.clear();
- showWelcome();
- }
-}
-
-
-// ---------------------------------------------------------------------------
-// Search (enhanced with autocomplete, keyboard nav, global shortcuts)
-// ---------------------------------------------------------------------------
-// ── Search toggle state ──
-
-export function initSearch() {
- const input = document.getElementById("search-input");
- if (!input) return;
- const caseBtn = document.getElementById("rb-case");
- const wordBtn = document.getElementById("rb-word");
- const regexBtn = document.getElementById("rb-regex");
- const filterBtn = document.getElementById("search-filter-btn");
- const clearBtn = document.getElementById("search-clear-btn");
- const filterRow = document.getElementById("search-filter-row");
- const prevBtn = document.getElementById("rb-prev");
- const nextBtn = document.getElementById("rb-next");
- const countEl = document.getElementById("rb-count");
- const resultsBar = document.getElementById("results-bar");
-
- function _showResultsBar() { if (resultsBar) resultsBar.style.display = 'flex'; }
- function _hideResultsBar() { if (resultsBar) resultsBar.style.display = 'none'; }
-
- function _updateToggleUI() {
- caseBtn.classList.toggle("active", state.searchCaseSensitive);
- wordBtn.classList.toggle("active", state.searchWholeWord);
- regexBtn.classList.toggle("active", state.searchRegex);
- filterBtn.classList.toggle("active", state.searchFilterVisible);
- }
-
- // Toggle buttons
- caseBtn.addEventListener("click", () => { state.searchCaseSensitive = !state.searchCaseSensitive; _updateToggleUI(); _research(); });
- if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); });
- if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); });
- if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
-
- // ── Result navigation (up/down arrows + Enter) ──
- let _searchResultIdx = -1;
- let _searchResultItems = [];
-
- function _updateResultHighlight() {
- _searchResultItems.forEach((el, i) => {
- el.classList.toggle("search-result-active", i === _searchResultIdx);
- });
- if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) {
- _searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" });
- }
- const countEl = document.getElementById("rb-count");
- if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`;
- }
-
- function _refreshResultItems() {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- _searchResultIdx = _searchResultItems.length > 0 ? 0 : -1;
- _updateResultHighlight();
- if (_searchResultItems.length > 0) _showResultsBar(); else _hideResultsBar();
- }
-
- window.navigateSearchResults = function(delta) {
- _searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
- if (_searchResultItems.length === 0) return;
- _searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta));
- _updateResultHighlight();
- };
-
- if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1));
- if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1));
-
- function _research() {
- const q = input.value.trim();
- if (q.length >= _getEffective("min_query_length", 2)) {
- clearTimeout(state.searchTimeout);
- state.searchTimeout = setTimeout(() => {
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- state.advancedSearchOffset = 0;
- performAdvancedSearch(q, vault, tagFilter);
- }, _getEffective("debounce_ms", 300));
- }
- }
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- if (e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
- else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
- else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
- else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
- }
- });
-
- // Initialize sub-controllers
- AutocompleteDropdown.init();
- SearchChips.init();
-
- // Initially hide clear button
- if (clearBtn) clearBtn.style.display = "none";
-
- // --- Input handler: autocomplete dropdown only (search triggered on Enter or selection) ---
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
-
- // Show autocomplete dropdown while typing
- AutocompleteDropdown.populate(input.value, input.selectionStart);
-
- // Clear results when input becomes empty
- if (!hasText) {
- SearchChips.clear();
- showWelcome();
- }
- });
-
- // --- Focus handler: show history dropdown ---
- input.addEventListener("focus", () => {
- if (input.value.length === 0) {
- const historyItems = SearchHistory.filter("").slice(0, 5);
- if (historyItems.length > 0) {
- AutocompleteDropdown.populate("", 0);
- }
- }
- });
-
- // --- Keyboard navigation in dropdown ---
- input.addEventListener("keydown", (e) => {
- if (AutocompleteDropdown.isVisible()) {
- if (e.key === "ArrowDown") {
- e.preventDefault();
- AutocompleteDropdown.navigateDown();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- AutocompleteDropdown.navigateUp();
- } else if (e.key === "Enter") {
- // First: check dropdown suggestions (higher priority than search results)
- if (AutocompleteDropdown.selectActive()) {
- e.preventDefault();
- return;
- }
- // Second: navigate search results if visible
- const results = document.querySelectorAll(".search-result-item");
- if (results.length > 0 && _searchResultIdx >= 0) {
- const el = results[_searchResultIdx];
- if (el) {
- const vault = el.dataset.vault;
- const path = el.dataset.path;
- if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; }
- }
- }
- // Third: execute search
- AutocompleteDropdown.hide();
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- } else if (e.key === "Escape") {
- AutocompleteDropdown.hide();
- e.stopPropagation();
- }
- } else {
- // Dropdown not visible: navigate search results or execute search
- if (e.key === "ArrowDown") {
- if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); }
- } else if (e.key === "ArrowUp") {
- if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); }
- } else if (e.key === "Enter") {
- const q = input.value.trim();
- if (q) {
- SearchHistory.add(q);
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
- performAdvancedSearch(q, vault, tagFilter);
- }
- e.preventDefault();
- }
- }
- });
-
- clearBtn.addEventListener("click", () => {
- input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- state.searchCaseSensitive = false;
- state.searchWholeWord = false;
- state.searchRegex = false;
- _updateToggleUI();
- _hideResultsBar();
- SearchChips.clear();
- AutocompleteDropdown.hide();
- showWelcome();
- });
-
- // --- Global keyboard shortcuts ---
- document.addEventListener("keydown", (e) => {
- // Ctrl+K or Cmd+K: focus search
- if ((e.ctrlKey || e.metaKey) && e.key === "k") {
- e.preventDefault();
- input.focus();
- input.select();
- }
- // "/" key: focus search (when not in an input/textarea)
- if (e.key === "/" && !_isInputFocused()) {
- e.preventDefault();
- input.focus();
- }
- // Escape: blur search input and close dropdown
- if (e.key === "Escape" && document.activeElement === input) {
- AutocompleteDropdown.hide();
- input.blur();
- }
- });
-}
-
-/** Check if user is focused on an input/textarea/contenteditable */
-function _isInputFocused() {
- const tag = document.activeElement?.tagName;
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
- return document.activeElement?.isContentEditable === true;
-}
-
-// --- Backward-compatible search (existing /api/search endpoint) ---
-export async function performSearch(query, vaultFilter, tagFilter) {
- if (state.searchAbortController) state.searchAbortController.abort();
- state.searchAbortController = new AbortController();
- const searchId = ++state.currentSearchId;
- showLoading();
- let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
- if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
- try {
- const data = await api(url, { signal: state.searchAbortController.signal });
- if (searchId !== state.currentSearchId) return;
- renderSearchResults(data, query, tagFilter);
- } catch (err) {
- if (err.name === "AbortError") return;
- if (searchId !== state.currentSearchId) return;
- showWelcome();
- } finally {
- hideProgressBar();
- if (searchId === state.currentSearchId) state.searchAbortController = null;
- }
-}
-
-// --- Advanced search with TF-IDF, facets, pagination ---
-export async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
- if (state.searchAbortController) state.searchAbortController.abort();
- state.searchAbortController = new AbortController();
- const searchId = ++state.currentSearchId;
- showLoading();
-
- const ofs = offset !== undefined ? offset : state.advancedSearchOffset;
- const sortBy = sort || state.advancedSearchSort;
- state.advancedSearchLastQuery = query;
-
- // Update chips from parsed query
- const parsed = QueryParser.parse(query);
- SearchChips.update(parsed);
-
- const effectiveLimit = _getEffective("results_per_page", state.ADVANCED_SEARCH_LIMIT);
- let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
- if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
- if (state.searchCaseSensitive) url += "&case_sensitive=true";
- if (state.searchWholeWord) url += "&whole_word=true";
- if (state.searchRegex) url += "®ex=true";
- const includeEl = document.getElementById("search-include-input");
- const excludeEl = document.getElementById("search-exclude-input");
- if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
- if (excludeEl?.value.trim()) url += `&exclude_paths=${encodeURIComponent(excludeEl.value.trim())}`;
- if (parsed.created) url += `&created=${encodeURIComponent(parsed.created)}`;
- if (parsed.modified) url += `&modified=${encodeURIComponent(parsed.modified)}`;
- if (parsed.size) url += `&size=${encodeURIComponent(parsed.size)}`;
-
- // Search timeout — abort if server takes too long
- const timeoutId = setTimeout(
- () => {
- if (state.searchAbortController) state.searchAbortController.abort();
- },
- _getEffective("search_timeout_ms", state.SEARCH_TIMEOUT_MS),
- );
-
- try {
- const data = await api(url, { signal: state.searchAbortController.signal });
- clearTimeout(timeoutId);
- if (searchId !== state.currentSearchId) return;
- state.advancedSearchTotal = data.total;
- state.advancedSearchOffset = ofs;
- renderAdvancedSearchResults(data, query, tagFilter);
- } catch (err) {
- clearTimeout(timeoutId);
- if (err.name === "AbortError") return;
- if (searchId !== state.currentSearchId) return;
-
- // Try offline search fallback
- console.log('[Search] Network failed, trying offline search');
- const offlineResults = await searchOffline(query, vaultFilter);
- if (offlineResults && offlineResults.length > 0) {
- const data = {
- query: query,
- vault_filter: vaultFilter,
- count: offlineResults.length,
- total: offlineResults.length,
- offset: 0,
- limit: effectiveLimit,
- results: offlineResults,
- offline: true
- };
- state.advancedSearchTotal = offlineResults.length;
- state.advancedSearchOffset = 0;
- renderAdvancedSearchResults(data, query, tagFilter);
- return;
- }
-
- showWelcome();
- } finally {
- hideProgressBar();
- if (searchId === state.currentSearchId) state.searchAbortController = null;
- }
-}
-
-// --- Legacy search results renderer (kept for backward compat) ---
-export function renderSearchResults(data, query, tagFilter) {
- const area = document.getElementById("content-area");
- area.innerHTML = "";
- const header = buildSearchResultsHeader(data, query, tagFilter);
- area.appendChild(header);
- if (data.results.length === 0) {
- area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode(t("search.no_results"))]));
- return;
- }
- const container = el("div", { class: "search-results" });
- data.results.forEach((r) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(r.path, r.vault)) {
- return; // Skip this result
- }
-
- const titleDiv = el("div", { class: "search-result-title" });
- if (query && query.trim()) {
- highlightSearchText(titleDiv, r.title, query, state.searchCaseSensitive);
- } else {
- titleDiv.textContent = r.title;
- }
- const snippetDiv = el("div", { class: "search-result-snippet" });
- if (r.snippet && r.snippet.includes("")) {
- snippetDiv.innerHTML = r.snippet;
- } else if (query && query.trim() && r.snippet) {
- highlightSearchText(snippetDiv, r.snippet, query, state.searchCaseSensitive);
- } else {
- snippetDiv.textContent = r.snippet || "";
- }
- const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
- el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
- titleDiv,
- el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]),
- snippetDiv
- ]);
- if (r.tags && r.tags.length > 0) {
- const tagsDiv = el("div", { class: "search-result-tags" });
- r.tags.forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", (e) => {
- e.stopPropagation();
- addTagFilter(tag);
- });
- tagsDiv.appendChild(tagEl);
- }
- });
- if (tagsDiv.children.length > 0) item.appendChild(tagsDiv);
- }
- item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); });
- container.appendChild(item);
- });
- area.appendChild(container);
-}
-
-// --- Advanced search results renderer (facets, highlighted snippets, pagination, sort) ---
-export function renderAdvancedSearchResults(data, query, tagFilter) {
- const area = document.getElementById("content-area");
- area.innerHTML = "";
-
- // Update match counter + show floating results bar
- var rb = document.getElementById("results-bar");
- var countEl = document.getElementById("rb-count");
- if (countEl) countEl.textContent = `${data.total > 0 ? "1" : "0"}/${data.total}`;
- if (rb) rb.style.display = 'flex';
-
- // Header with result count and sort controls
- const header = el("div", { class: "search-results-header" });
- const summaryText = el("span", { class: "search-results-summary-text" });
- const parsed = QueryParser.parse(query);
- const freeText = parsed.freeText;
-
- if (freeText && tagFilter) {
- summaryText.textContent = t('search.results_filtered', {count: data.total, query: freeText});
- } else if (freeText) {
- summaryText.textContent = t('search.results_for', {count: data.total, query: freeText});
- } else if (parsed.tags.length > 0 || tagFilter) {
- summaryText.textContent = t('search.files_filtered', {count: data.total});
- } else {
- summaryText.textContent = t('search.results_count', {count: data.total});
- }
- if (data.query_time_ms !== undefined && data.query_time_ms > 0) {
- const timeBadge = el("span", { class: "search-time-badge" });
- timeBadge.textContent = t('search.query_time', {time: data.query_time_ms});
- summaryText.appendChild(timeBadge);
- }
- header.appendChild(summaryText);
-
- // Active filter badges
- const filtersRow = el("div", { class: "search-filters-row" });
- if (state.searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
- if (state.searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
- if (state.searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
- const inclEl = document.getElementById("search-include-input");
- const exclEl = document.getElementById("search-exclude-input");
- if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())]));
- if (exclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("excl: " + exclEl.value.trim())]));
- if (filtersRow.children.length > 0) header.appendChild(filtersRow);
-
- // Sort controls
- const sortDiv = el("div", { class: "search-sort" });
- const btnRelevance = el("button", { class: "search-sort__btn" + (state.advancedSearchSort === "relevance" ? " active" : ""), type: "button" });
- btnRelevance.textContent = "Pertinence";
- btnRelevance.addEventListener("click", () => {
- state.advancedSearchSort = "relevance";
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, 0, "relevance");
- });
- const btnDate = el("button", { class: "search-sort__btn" + (state.advancedSearchSort === "modified" ? " active" : ""), type: "button" });
- btnDate.textContent = "Date";
- btnDate.addEventListener("click", () => {
- state.advancedSearchSort = "modified";
- state.advancedSearchOffset = 0;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, 0, "modified");
- });
- sortDiv.appendChild(btnRelevance);
- sortDiv.appendChild(btnDate);
- header.appendChild(sortDiv);
-
- // Save search button
- const saveBtn = el("button", { class: "search-save-btn", type: "button", title: t("search.save_search") });
- saveBtn.innerHTML = ' Sauver';
- saveBtn.addEventListener("click", async () => {
- const inclEl = document.getElementById("search-include-input");
- const exclEl = document.getElementById("search-exclude-input");
- try {
- await api("/api/saved-searches", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- query: query,
- vault: document.getElementById("vault-filter")?.value || "all",
- case_sensitive: state.searchCaseSensitive,
- whole_word: state.searchWholeWord,
- regex: state.searchRegex,
- include_paths: inclEl?.value || "",
- exclude_paths: exclEl?.value || "",
- }),
- });
- showToast(t("search.saved"), "success");
- loadSavedSearches();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
- header.appendChild(saveBtn);
- area.appendChild(header);
-
- // Active sidebar tag chips
- if (state.selectedTags.length > 0) {
- const activeTags = el("div", { class: "search-results-active-tags" });
- state.selectedTags.forEach((tag) => {
- const removeBtn = el(
- "button",
- {
- class: "search-results-active-tag-remove",
- title: `Retirer ${tag} du filtre`,
- },
- [document.createTextNode("×")],
- );
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeTagFilter(tag);
- });
- const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
- activeTags.appendChild(chip);
- });
- area.appendChild(activeTags);
- }
-
- // Facets panel
- if (data.facets && (Object.keys(data.facets.tags || {}).length > 0 || Object.keys(data.facets.vaults || {}).length > 0)) {
- const facetsDiv = el("div", { class: "search-facets" });
-
- // Vault facets
- const vaultFacets = data.facets.vaults || {};
- if (Object.keys(vaultFacets).length > 1) {
- const group = el("div", { class: "search-facets__group" });
- const label = el("span", { class: "search-facets__label" });
- label.textContent = "Vaults";
- group.appendChild(label);
- for (const [vaultName, count] of Object.entries(vaultFacets)) {
- const item = el("span", { class: "search-facets__item" });
- item.innerHTML = `${vaultName} ${count} `;
- item.addEventListener("click", () => {
- const input = document.getElementById("search-input");
- // Add vault: operator
- const current = input.value.replace(/vault:\S+\s*/gi, "").trim();
- input.value = current + " vault:" + vaultName;
- _triggerAdvancedSearch(input.value);
- });
- group.appendChild(item);
- }
- facetsDiv.appendChild(group);
- }
-
- // Tag facets
- const tagFacets = data.facets.tags || {};
- if (Object.keys(tagFacets).length > 0) {
- const group = el("div", { class: "search-facets__group" });
- const label = el("span", { class: "search-facets__label" });
- label.textContent = "Tags";
- group.appendChild(label);
- const entries = Object.entries(tagFacets).slice(0, 12);
- for (const [tagName, count] of entries) {
- const item = el("span", { class: "search-facets__item" });
- item.innerHTML = `#${tagName} ${count} `;
- item.addEventListener("click", () => {
- addTagFilter(tagName);
- });
- group.appendChild(item);
- }
- facetsDiv.appendChild(group);
- }
-
- area.appendChild(facetsDiv);
- }
-
- // Empty state
- if (data.results.length === 0) {
- area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode(t("search.no_results"))]));
- return;
- }
-
- // Results list
- const container = el("div", { class: "search-results" });
- data.results.forEach((r) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(r.path, r.vault)) {
- return; // Skip this result
- }
-
- const titleDiv = el("div", { class: "search-result-title" });
- if (freeText) {
- highlightSearchText(titleDiv, r.title, freeText, state.searchCaseSensitive);
- } else {
- titleDiv.textContent = r.title;
- }
-
- // Snippet — use HTML from backend (already has tags)
- const snippetDiv = el("div", { class: "search-result-snippet search-result__snippet" });
- if (r.snippet && r.snippet.includes("")) {
- snippetDiv.innerHTML = r.snippet;
- } else if (freeText && r.snippet) {
- highlightSearchText(snippetDiv, r.snippet, freeText, state.searchCaseSensitive);
- } else {
- snippetDiv.textContent = r.snippet || "";
- }
-
- // Score badge
- const scoreEl = el("span", { class: "search-result-score", style: "font-size:0.7rem;color:var(--text-muted);margin-left:8px" });
- scoreEl.textContent = `score: ${r.score}`;
-
- const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]);
-
- const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
- el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
- titleDiv, vaultPath, snippetDiv
- ]);
-
- if (r.tags && r.tags.length > 0) {
- const tagsDiv = el("div", { class: "search-result-tags" });
- r.tags.forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", (e) => {
- e.stopPropagation();
- addTagFilter(tag);
- });
- tagsDiv.appendChild(tagEl);
- }
- });
- if (tagsDiv.children.length > 0) item.appendChild(tagsDiv);
- }
-
- item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); });
- container.appendChild(item);
- });
- area.appendChild(container);
-
- // Pagination
- if (data.total > state.ADVANCED_SEARCH_LIMIT) {
- const paginationDiv = el("div", { class: "search-pagination" });
- const prevBtn = el("button", { class: "search-pagination__btn", type: "button" });
- prevBtn.textContent = t('search.previous');
- prevBtn.disabled = state.advancedSearchOffset === 0;
- prevBtn.addEventListener("click", () => {
- state.advancedSearchOffset = Math.max(0, state.advancedSearchOffset - state.ADVANCED_SEARCH_LIMIT);
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset);
- document.getElementById("content-area").scrollTop = 0;
- });
-
- const info = el("span", { class: "search-pagination__info" });
- const from = state.advancedSearchOffset + 1;
- const to = Math.min(state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT, data.total);
- info.textContent = `${from}–${to} sur ${data.total}`;
-
- const nextBtn = el("button", { class: "search-pagination__btn", type: "button" });
- nextBtn.textContent = "Suivant →";
- nextBtn.disabled = state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT >= data.total;
- nextBtn.addEventListener("click", () => {
- state.advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT;
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset);
- document.getElementById("content-area").scrollTop = 0;
- });
-
- paginationDiv.appendChild(prevBtn);
- paginationDiv.appendChild(info);
- paginationDiv.appendChild(nextBtn);
- area.appendChild(paginationDiv);
- }
-
- safeCreateIcons();
- // Initialize result navigation (select first result)
- setTimeout(() => { if (window.navigateSearchResults) window.navigateSearchResults(0); }, 50);
-}
-
-
diff --git a/desktop/frontend-static/js/sidebar.js b/desktop/frontend-static/js/sidebar.js
deleted file mode 100644
index b4146e0..0000000
--- a/desktop/frontend-static/js/sidebar.js
+++ /dev/null
@@ -1,1124 +0,0 @@
-import { state } from './state.js';
-import { safeCreateIcons, getFileIcon, flushIcons } from './utils.js';
-import { api } from './auth.js';
-import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js';
-import { _populateRecentVaultFilter, switchSidebarTab } from './config.js';
-import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText } from './viewer.js';
-import { performAdvancedSearch } from './search.js';
-import { t } from './i18n.js';
-
-// ---------------------------------------------------------------------------
-// Vault context switching
-// ---------------------------------------------------------------------------
-function initVaultContext() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- if (!filter || !quickSelect) return;
-
- filter.addEventListener("change", async () => {
- await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" });
- });
-
- quickSelect.addEventListener("change", async () => {
- await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" });
- });
-}
-
-async function setSelectedVaultContext(vaultName, options) {
- state.selectedContextVault = vaultName;
- state.showingSource = false;
- state.cachedRawSource = null;
- syncVaultSelectors();
- await refreshSidebarForContext();
- await refreshTagsForContext();
-
- // Synchroniser le dashboard et les fichiers récents
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.load) {
- DashboardRecentWidget.load(vaultName);
- }
- if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
- DashboardBookmarkWidget.load(vaultName);
- }
- if (state.activeSidebarTab === "recent") {
- loadRecentFiles(vaultName === "all" ? null : vaultName);
- }
-
- showWelcome();
- // If a specific vault is selected, switch to vault home page
- if (vaultName !== "all") {
- import('./vaulthome.js').then(m => m.showVaultHome(vaultName, ''));
- }
- if (options && options.focusVault && vaultName !== "all") {
- await focusVaultInSidebar(vaultName);
- }
-}
-
-function syncVaultSelectors() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- const recentFilter = document.getElementById("recent-vault-filter");
- const dashboardFilter = document.getElementById("dashboard-vault-filter");
- const contextText = document.getElementById("vault-context-text");
-
- if (filter) filter.value = state.selectedContextVault;
- if (quickSelect) quickSelect.value = state.selectedContextVault;
- if (recentFilter) recentFilter.value = state.selectedContextVault === "all" ? "" : state.selectedContextVault;
- if (dashboardFilter) dashboardFilter.value = state.selectedContextVault;
-
- // Mise à jour visuelle des dropdowns personnalisés
- updateCustomDropdownVisual("vault-filter-dropdown", state.selectedContextVault);
- updateCustomDropdownVisual("vault-quick-select-dropdown", state.selectedContextVault);
-
- // Update vault context indicator
- if (contextText) {
- contextText.textContent = state.selectedContextVault === "all" ? "All" : state.selectedContextVault;
- }
- // Apply color class to the indicator
- const indicator = document.getElementById("vault-context-indicator");
- if (indicator) {
- indicator.classList.remove("vault-ctx-all", "vault-ctx-vault", "vault-ctx-dir");
- if (state.selectedContextVault === "all") {
- indicator.classList.add("vault-ctx-all");
- } else {
- indicator.classList.add("vault-ctx-vault");
- }
- }
-}
-
-/**
- * Updates the visual state of a custom dropdown based on its current value.
- */
-function updateCustomDropdownVisual(dropdownId, value) {
- const dropdown = document.getElementById(dropdownId);
- if (!dropdown) return;
-
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
- const options = dropdown.querySelectorAll(".custom-dropdown-option");
-
- options.forEach((opt) => {
- const optValue = opt.getAttribute("data-value");
- if (optValue === value) {
- opt.classList.add("selected");
- if (selectedText) selectedText.textContent = opt.textContent;
- } else {
- opt.classList.remove("selected");
- }
- });
-}
-
-function scrollTreeItemIntoView(element, alignToTop) {
- if (!element) return;
- const scrollContainer = document.getElementById("sidebar-panel-vaults");
- if (!scrollContainer) return;
-
- const containerRect = scrollContainer.getBoundingClientRect();
- const elementRect = element.getBoundingClientRect();
- const isAbove = elementRect.top < containerRect.top;
- const isBelow = elementRect.bottom > containerRect.bottom;
-
- if (!isAbove && !isBelow && !alignToTop) return;
-
- const currentTop = scrollContainer.scrollTop;
- const offsetTop = element.offsetTop;
- const shouldCenter = alignToTop === "center";
- const centeredTop = Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height / 2) + (elementRect.height / 2));
- const targetTop = shouldCenter
- ? centeredTop
- : alignToTop
- ? Math.max(0, offsetTop - 60)
- : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35);
-
- scrollContainer.scrollTo({
- top: targetTop,
- behavior: "smooth",
- });
-}
-
-async function refreshSidebarForContext() {
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- const vaultsToShow = state.selectedContextVault === "all" ? state.allVaults : state.allVaults.filter((v) => v.name === state.selectedContextVault);
-
- vaultsToShow.forEach((v) => {
- const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- safeCreateIcons();
-}
-
-async function focusVaultInSidebar(vaultName) {
- switchSidebarTab("vaults");
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (childContainer && childContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
- scrollTreeItemIntoView(vaultItem, false);
-}
-
-async function refreshTagsForContext() {
- const vaultParam = state.selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(state.selectedContextVault)}`;
- const data = await api(`/api/tags${vaultParam}`);
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
-}
-
-// ---------------------------------------------------------------------------
-// Helper: Check if path should be displayed based on hideHiddenFiles setting
-// ---------------------------------------------------------------------------
-function shouldDisplayPath(path, vaultName) {
- // Get hideHiddenFiles setting for this vault (default: false = show all)
- const settings = state.vaultSettings[vaultName] || { hideHiddenFiles: false };
-
- if (!settings.hideHiddenFiles) {
- // Show all files
- return true;
- }
-
- // Check if any segment of the path starts with a dot (hidden)
- const segments = path.split("/").filter(Boolean);
- for (const segment of segments) {
- if (segment.startsWith(".")) {
- return false; // Hide this path
- }
- }
-
- return true; // Show this path
-}
-
-async function loadVaultSettings() {
- try {
- const settings = await api("/api/vaults/settings/all");
- state.vaultSettings = settings;
- } catch (err) {
- console.error("Failed to load vault settings:", err);
- state.vaultSettings = {};
- }
-}
-
-// ---------------------------------------------------------------------------
-// Sidebar — Vault tree
-// ---------------------------------------------------------------------------
-async function loadVaults() {
- const vaults = await api("/api/vaults");
- state.allVaults = vaults;
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- // Prepare dropdown options
- const dropdownOptions = [{ value: "all", text: t('help.all_vaults') }, ...vaults.map((v) => ({ value: v.name, text: v.name }))];
-
- // Populate custom dropdowns
- populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all");
- populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all");
-
- // Populate standard selects
- _populateRecentVaultFilter();
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.populateVaultFilter) {
- DashboardRecentWidget.populateVaultFilter();
- }
-
- vaults.forEach((v) => {
- // Sidebar tree entry
- const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- syncVaultSelectors();
- safeCreateIcons();
-}
-
-/**
- * Refreshes the sidebar tree while preserving the expanded state of vaults and folders.
- * Optimized to avoid a full sidebar wipe and minimize visible loading states.
- */
-/**
- * Incrementally update a directory container without wiping existing DOM.
- * Only adds new items, removes deleted ones, and updates changed ones.
- */
-async function incrementalLoadDirectory(vaultName, dirPath, container) {
- let data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- // Server unavailable — keep existing content
- return;
- }
-
- // Build a map of existing DOM elements by path
- const existingItems = {};
- const existingChildren = {}; // path -> child container (for directories)
- for (let i = 0; i < container.children.length; i++) {
- const child = container.children[i];
- if (child.classList.contains("tree-item") && child.dataset.path) {
- existingItems[child.dataset.path] = child;
- // The next sibling should be the tree-children container for this directory
- if (i + 1 < container.children.length) {
- const next = container.children[i + 1];
- if (next.classList.contains("tree-children")) {
- existingChildren[child.dataset.path] = next;
- }
- }
- }
- }
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- if (!shouldDisplayPath(item.path, vaultName)) return;
-
- const existing = existingItems[item.path];
-
- if (existing) {
- // Item already exists — reuse it, but update text/badge if needed
- const textEl = existing.querySelector(".tree-item-text");
- const displayName = item.type === "file" && item.name.match(/\.md$/i)
- ? item.name.replace(/\.md$/i, "")
- : item.name;
- if (textEl && textEl.textContent !== displayName) {
- textEl.textContent = displayName;
- }
- // Update badge for directories
- if (item.type === "directory") {
- const badge = existing.querySelector(".badge-small");
- const newBadge = `(${item.children_count})`;
- if (badge && badge.textContent !== newBadge) {
- badge.textContent = newBadge;
- } else if (!badge) {
- existing.appendChild(smallBadge(item.children_count));
- }
- }
- fragment.appendChild(existing);
- // Also re-add the child container for directories
- if (item.type === "directory" && existingChildren[item.path]) {
- fragment.appendChild(existingChildren[item.path]);
- } else if (item.type === "directory") {
- // Directory existed but no child container — create one
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
- }
- } else {
- // New item — create it
- 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- if (window.innerWidth <= 768) {
- TabManager.openPersistent(vaultName, item.path);
- } else {
- TabManager.openPreview(vaultName, item.path);
- }
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
- });
-
- fragment.appendChild(fileItem);
- }
- }
- });
-
- // Replace container content in a single batch operation to avoid flash
- container.textContent = "";
- container.appendChild(fragment);
-}
-
-export async function refreshSidebarTreePreservingState() {
- // 1. Capture expanded states
- const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
- .filter((v) => {
- const children = document.getElementById(`vault-children-${v.dataset.vault}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((v) => v.dataset.vault);
-
- const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]"))
- .filter((item) => {
- const vault = item.dataset.vault;
- const path = item.dataset.path;
- const children = document.getElementById(`dir-${vault}-${path}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path }));
-
- const selectedItem = document.querySelector(".tree-item.path-selected");
- const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
-
- // 2. Soft update: vault names/counts without wiping the tree
- try {
- const vaults = await api("/api/vaults");
- state.allVaults = vaults;
- vaults.forEach((v) => {
- const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`);
- if (vItem) {
- const badge = vItem.querySelector(".badge-small");
- if (badge) badge.textContent = `(${v.file_count})`;
- }
- });
- } catch (e) {
- console.warn("Soft vault refresh failed, falling back to full reload", e);
- await loadVaults();
- return;
- }
-
- // 3. Incrementally update expanded vaults (no DOM wipe)
- for (const vName of expandedVaults) {
- const container = document.getElementById(`vault-children-${vName}`);
- if (container) {
- await incrementalLoadDirectory(vName, "", container);
- }
- }
-
- // 4. Incrementally update expanded directories (parents first, no DOM wipe)
- expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
- for (const dir of expandedDirs) {
- const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
- if (container) {
- try {
- await incrementalLoadDirectory(dir.vault, dir.path, container);
- container.classList.remove("collapsed");
- const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
- if (dItem) {
- const chev = dItem.querySelector("[data-lucide]");
- if (chev) chev.setAttribute("data-lucide", "chevron-down");
- }
- } catch (e) {
- console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
- }
- }
- }
-
- // 5. Restore selection
- if (selectedState) {
- await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false });
- }
-
- safeCreateIcons();
-}
-
-async function toggleVault(itemEl, vaultName, forceExpand) {
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!childContainer) return;
-
- scrollTreeItemIntoView(itemEl, false);
-
- const shouldExpand = forceExpand || childContainer.classList.contains("collapsed");
-
- if (shouldExpand) {
- // Expand — load children if empty
- if (childContainer.children.length === 0) {
- await loadDirectory(vaultName, "", childContainer);
- }
- childContainer.classList.remove("collapsed");
- // Swap chevron
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- safeCreateIcons();
- } else {
- childContainer.classList.add("collapsed");
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-right");
- safeCreateIcons();
- }
-}
-
-async function expandDirectoryInSidebar(vaultName, dirPath, dirItem) {
- const subContainer = document.getElementById(`dir-${vaultName}-${dirPath}`);
- if (!subContainer) return null;
-
- if (subContainer.children.length === 0) {
- await loadDirectory(vaultName, dirPath, subContainer);
- }
-
- subContainer.classList.remove("collapsed");
- if (dirItem) {
- const chevron = dirItem.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- }
- safeCreateIcons();
- return subContainer;
-}
-
-async function focusPathInSidebar(vaultName, targetPath, options) {
- switchSidebarTab("vaults");
-
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
-
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
-
- const vaultContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!vaultContainer) return;
-
- if (vaultContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
-
- if (!targetPath) {
- // Clear any previous path selection
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- scrollTreeItemIntoView(vaultItem, options && options.alignToTop);
- return;
- }
-
- const segments = targetPath.split("/").filter(Boolean);
- let currentContainer = vaultContainer;
- let cumulativePath = "";
- let lastTargetItem = null;
-
- for (let index = 0; index < segments.length; index++) {
- cumulativePath += (cumulativePath ? "/" : "") + segments[index];
-
- let targetItem = null;
- try {
- targetItem = currentContainer.querySelector(`.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(cumulativePath)}"]`);
- } catch (e) {
- targetItem = null;
- }
-
- if (!targetItem) {
- return;
- }
-
- lastTargetItem = targetItem;
-
- const isLastSegment = index === segments.length - 1;
- if (!isLastSegment) {
- const nextContainer = await expandDirectoryInSidebar(vaultName, cumulativePath, targetItem);
- if (nextContainer) {
- currentContainer = nextContainer;
- }
- }
- }
-
- if (lastTargetItem && options && options.expandTarget) {
- await expandDirectoryInSidebar(vaultName, targetPath, lastTargetItem);
- }
-
- // Clear previous path selections and highlight the final target
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- if (lastTargetItem) {
- lastTargetItem.classList.add("path-selected");
- }
-
- scrollTreeItemIntoView(lastTargetItem, options && options.alignToTop);
-}
-
-function getParentDirectoryPath(filePath) {
- if (!filePath) return "";
- const segments = filePath.split("/").filter(Boolean);
- if (segments.length <= 1) return "";
- segments.pop();
- return segments.join("/");
-}
-
-function syncActiveFileTreeItem(vaultName, filePath) {
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
- if (!vaultName || !filePath) return;
- const selector = `.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(filePath)}"]`;
- try {
- const active = document.querySelector(selector);
- if (active) active.classList.add("active");
- } catch (e) {
- /* selector might fail on special chars */
- }
-}
-
-async function loadDirectory(vaultName, dirPath, container) {
- // Only show the loading spinner if the container is currently empty
- const isEmpty = container.children.length === 0;
- if (isEmpty) {
- container.innerHTML = '';
- }
-
- var data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- container.innerHTML = 'Erreur de chargement
';
- return;
- }
- container.innerHTML = "";
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(item.path, vaultName)) {
- return; // Skip this 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- if (window.innerWidth <= 768) {
- TabManager.openPersistent(vaultName, item.path);
- } else {
- TabManager.openPreview(vaultName, item.path);
- }
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly);
- });
-
- fragment.appendChild(fileItem);
- }
- });
-
- container.appendChild(fragment);
- safeCreateIcons();
-}
-
-// ---------------------------------------------------------------------------
-// Sidebar filter
-// ---------------------------------------------------------------------------
-function initSidebarFilter() {
- const input = document.getElementById("sidebar-filter-input");
- const caseBtn = document.getElementById("sidebar-filter-case-btn");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
-
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
- clearTimeout(state.filterDebounce);
- state.filterDebounce = setTimeout(async () => {
- const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (hasText) {
- if (state.activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- } else {
- if (state.activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- }
- }, 220);
- });
-
- caseBtn.addEventListener("click", async () => {
- state.sidebarFilterCaseSensitive = !state.sidebarFilterCaseSensitive;
- caseBtn.classList.toggle("active");
- const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (input.value.trim()) {
- if (state.activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- }
- });
-
- clearBtn.addEventListener("click", async () => {
- input.value = "";
- clearBtn.style.display = "none";
- state.sidebarFilterCaseSensitive = false;
- caseBtn.classList.remove("active");
- clearTimeout(state.filterDebounce);
- if (state.activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- });
-
- clearBtn.style.display = "none";
-}
-
-async function performTreeSearch(query) {
- if (!query) {
- await restoreSidebarTree();
- return;
- }
-
- try {
- const vaultParam = state.selectedContextVault === "all" ? "all" : state.selectedContextVault;
- const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`;
- const data = await api(url);
- renderFilteredSidebarResults(query, data.results);
- } catch (err) {
- console.error("Tree search error:", err);
- renderFilteredSidebarResults(query, []);
- }
-}
-
-async function restoreSidebarTree() {
- await refreshSidebarForContext();
- if (state.currentVault) {
- focusPathInSidebar(state.currentVault, state.currentPath || "", { alignToTop: false }).catch(() => {});
- }
-}
-
-function renderFilteredSidebarResults(query, results) {
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- const grouped = new Map();
- results.forEach((result) => {
- if (!grouped.has(result.vault)) {
- grouped.set(result.vault, []);
- }
- grouped.get(result.vault).push(result);
- });
-
- if (grouped.size === 0) {
- container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode(t("sidebar.no_results"))]));
- return;
- }
-
- grouped.forEach((entries, vaultName) => {
- entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" }));
-
- const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]);
- container.appendChild(vaultHeader);
-
- const resultsWrapper = el("div", { class: "filter-results-group" });
- entries.forEach((entry) => {
- const resultItem = el(
- "div",
- {
- class: `tree-item filter-result-item filter-result-${entry.type}`,
- "data-vault": entry.vault,
- "data-path": entry.path,
- "data-type": entry.type,
- },
- [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)],
- );
-
- const textWrap = el("div", { class: "filter-result-text" });
- const primary = el("div", { class: "filter-result-primary" });
- appendHighlightedText(primary, entry.name, query, state.sidebarFilterCaseSensitive);
- const secondary = el("div", { class: "filter-result-secondary" });
- appendHighlightedText(secondary, entry.path, query, state.sidebarFilterCaseSensitive);
- textWrap.appendChild(primary);
- textWrap.appendChild(secondary);
- resultItem.appendChild(textWrap);
-
- resultItem.addEventListener("click", async () => {
- const input = document.getElementById("sidebar-filter-input");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
- if (input) input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- await restoreSidebarTree();
- if (entry.type === "directory") {
- await focusPathInSidebar(entry.vault, entry.path, { alignToTop: true, expandTarget: true });
- } else {
- if (window.innerWidth <= 768) {
- await TabManager.openPersistent(entry.vault, entry.path);
- } else {
- await TabManager.openPreview(entry.vault, entry.path);
- }
- await focusPathInSidebar(entry.vault, getParentDirectoryPath(entry.path), { alignToTop: true, expandTarget: true });
- syncActiveFileTreeItem(entry.vault, entry.path);
- }
- closeMobileSidebar();
- });
-
- resultsWrapper.appendChild(resultItem);
- });
-
- container.appendChild(resultsWrapper);
- });
-
- flushIcons();
-}
-
-function filterSidebarTree(query) {
- const tree = document.getElementById("vault-tree");
- const items = tree.querySelectorAll(".tree-item");
- const containers = tree.querySelectorAll(".tree-children");
-
- if (!query) {
- items.forEach((item) => item.classList.remove("filtered-out"));
- containers.forEach((c) => {
- c.classList.remove("filtered-out");
- // Keep current collapsed state when clearing filter
- });
- return;
- }
-
- // First pass: mark all as filtered out
- items.forEach((item) => item.classList.add("filtered-out"));
- containers.forEach((c) => c.classList.add("filtered-out"));
-
- // Second pass: find matching items and mark them + ancestors + descendants
- const matchingItems = new Set();
-
- items.forEach((item) => {
- const text = state.sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase();
- const searchQuery = state.sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (text.includes(searchQuery)) {
- matchingItems.add(item);
- 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;
- }
-
- // If this is a directory (has a children container after it), show all descendants
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- // Recursively show all children in this container
- showAllDescendants(nextEl);
- }
- }
- });
-
- // Third pass: show items that are descendants of matching directories
- // and ensure their containers are visible
- matchingItems.forEach((item) => {
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- const children = nextEl.querySelectorAll(".tree-item");
- children.forEach((child) => child.classList.remove("filtered-out"));
- }
- });
-}
-
-function showAllDescendants(container) {
- const items = container.querySelectorAll(".tree-item");
- items.forEach((item) => {
- item.classList.remove("filtered-out");
- // If this item has children, also show them
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- }
- });
- // Also ensure all nested containers are visible
- const nestedContainers = container.querySelectorAll(".tree-children");
- nestedContainers.forEach((c) => {
- c.classList.remove("filtered-out");
- c.classList.remove("collapsed");
- });
-}
-
-function filterTagCloud(query) {
- const tags = document.querySelectorAll("#tag-cloud .tag-item");
- tags.forEach((tag) => {
- const text = state.sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase();
- const searchQuery = state.sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (!query || text.includes(searchQuery)) {
- tag.classList.remove("filtered-out");
- } else {
- tag.classList.add("filtered-out");
- }
- });
-}
-
-// ---------------------------------------------------------------------------
-// Tag Filter Service
-// ---------------------------------------------------------------------------
-const TagFilterService = {
- defaultFilters: [
- { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true },
- { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true },
- { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true },
- ],
-
- getConfig() {
- const stored = localStorage.getItem("obsigate-tag-filters");
- if (stored) {
- try {
- return JSON.parse(stored);
- } catch (e) {
- return { tagFilters: this.defaultFilters };
- }
- }
- return { tagFilters: this.defaultFilters };
- },
-
- saveConfig(config) {
- localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
- },
-
- patternToRegex(pattern) {
- // 1. Escape ALL special regex characters
- // We use a broader set including * and .
- let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-
- // 2. Convert escaped '*' to '.*' (wildcard)
- regex = regex.replace(/\\\*/g, ".*");
-
- // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*'
- // We also handle optional whitespace around it to make it more user-friendly
- regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*");
-
- return regex;
- },
-
- isTagFiltered(tag) {
- const config = this.getConfig();
- const filters = config.tagFilters || this.defaultFilters;
- const tagWithHash = `#${tag}`;
-
- for (const filter of filters) {
- if (!filter.enabled) continue;
- try {
- // Robustly handle regex with or without ^/$
- let patternStr = filter.regex;
- if (!patternStr.startsWith("^")) patternStr = "^" + patternStr;
- if (!patternStr.endsWith("$")) patternStr = patternStr + "$";
-
- const regex = new RegExp(patternStr);
- if (regex.test(tagWithHash)) {
- return true;
- }
- } catch (e) {
- console.warn("Invalid regex:", filter.regex, e);
- }
- }
- return false;
- },
-
- filterTags(tags) {
- const filtered = {};
- Object.entries(tags).forEach(([tag, count]) => {
- if (!this.isTagFiltered(tag)) {
- filtered[tag] = count;
- }
- });
- return filtered;
- },
-};
-
-// ---------------------------------------------------------------------------
-// Tags
-// ---------------------------------------------------------------------------
-async function loadTags() {
- const data = await api("/api/tags");
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
-}
-
-function renderTagCloud(tags) {
- const cloud = document.getElementById("tag-cloud");
- cloud.innerHTML = "";
-
- const counts = Object.values(tags);
- if (counts.length === 0) return;
-
- const maxCount = Math.max(...counts);
- const minSize = 0.7;
- const maxSize = 1.25;
-
- Object.entries(tags).forEach(([tag, count]) => {
- const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0;
- const size = minSize + ratio * (maxSize - minSize);
- const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", () => searchByTag(tag));
- cloud.appendChild(tagEl);
- });
-}
-
-function addTagFilter(tag) {
- if (!state.selectedTags.includes(tag)) {
- state.selectedTags.push(tag);
- performTagSearch();
- }
-}
-
-function removeTagFilter(tag) {
- state.selectedTags = state.selectedTags.filter((t) => t !== tag);
- if (state.selectedTags.length > 0) {
- performTagSearch();
- } else {
- const input = document.getElementById("search-input");
- if (input.value.trim()) {
- performAdvancedSearch(input.value.trim(), document.getElementById("vault-filter").value, null);
- } else {
- showWelcome();
- }
- }
-}
-
-function performTagSearch() {
- const input = document.getElementById("search-input");
- const query = input.value.trim();
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, state.selectedTags.length > 0 ? state.selectedTags.join(",") : null);
-}
-
-function buildSearchResultsHeader(data, query, tagFilter) {
- const header = el("div", { class: "search-results-header" });
- const summaryText = el("span", { class: "search-results-summary-text" });
-
- if (query && tagFilter) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`;
- } else if (query) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}"`;
- } else if (tagFilter) {
- summaryText.textContent = `${data.count} fichier(s) avec les tags`;
- } else {
- summaryText.textContent = `${data.count} résultat(s)`;
- }
-
- header.appendChild(summaryText);
-
- if (state.selectedTags.length > 0) {
- const activeTags = el("div", { class: "search-results-active-tags" });
- state.selectedTags.forEach((tag) => {
- const removeBtn = el(
- "button",
- {
- class: "search-results-active-tag-remove",
- title: `Retirer ${tag} du filtre`,
- "aria-label": `Retirer ${tag} du filtre`,
- },
- [document.createTextNode("×")],
- );
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeTagFilter(tag);
- });
-
- const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
- activeTags.appendChild(chip);
- });
- header.appendChild(activeTags);
- }
-
- return header;
-}
-
-function searchByTag(tag) {
- addTagFilter(tag);
-}
-
-
-export { initVaultContext, setSelectedVaultContext, syncVaultSelectors, shouldDisplayPath, loadVaults, loadVaultSettings, initSidebarFilter, TagFilterService, loadTags, scrollTreeItemIntoView, refreshSidebarForContext, focusVaultInSidebar, focusPathInSidebar, refreshTagsForContext, syncActiveFileTreeItem, searchByTag, addTagFilter, buildSearchResultsHeader, removeTagFilter };
diff --git a/desktop/frontend-static/js/sidebar_raw.js b/desktop/frontend-static/js/sidebar_raw.js
deleted file mode 100644
index 2c5a070..0000000
--- a/desktop/frontend-static/js/sidebar_raw.js
+++ /dev/null
@@ -1,1087 +0,0 @@
-// ---------------------------------------------------------------------------
-// Vault context switching
-// ---------------------------------------------------------------------------
-function initVaultContext() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- if (!filter || !quickSelect) return;
-
- filter.addEventListener("change", async () => {
- await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" });
- });
-
- quickSelect.addEventListener("change", async () => {
- await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" });
- });
-}
-
-async function setSelectedVaultContext(vaultName, options) {
- selectedContextVault = vaultName;
- showingSource = false;
- cachedRawSource = null;
- syncVaultSelectors();
- await refreshSidebarForContext();
- await refreshTagsForContext();
-
- // Synchroniser le dashboard et les fichiers récents
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.load) {
- DashboardRecentWidget.load(vaultName);
- }
- if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
- DashboardBookmarkWidget.load(vaultName);
- }
- if (activeSidebarTab === "recent") {
- loadRecentFiles(vaultName === "all" ? null : vaultName);
- }
-
- showWelcome();
- if (options && options.focusVault && vaultName !== "all") {
- await focusVaultInSidebar(vaultName);
- }
-}
-
-function syncVaultSelectors() {
- const filter = document.getElementById("vault-filter");
- const quickSelect = document.getElementById("vault-quick-select");
- const recentFilter = document.getElementById("recent-vault-filter");
- const dashboardFilter = document.getElementById("dashboard-vault-filter");
- const contextText = document.getElementById("vault-context-text");
-
- if (filter) filter.value = selectedContextVault;
- if (quickSelect) quickSelect.value = selectedContextVault;
- if (recentFilter) recentFilter.value = selectedContextVault === "all" ? "" : selectedContextVault;
- if (dashboardFilter) dashboardFilter.value = selectedContextVault;
-
- // Mise à jour visuelle des dropdowns personnalisés
- updateCustomDropdownVisual("vault-filter-dropdown", selectedContextVault);
- updateCustomDropdownVisual("vault-quick-select-dropdown", selectedContextVault);
-
- // Update vault context indicator
- if (contextText) {
- contextText.textContent = selectedContextVault === "all" ? "All" : selectedContextVault;
- }
-}
-
-/**
- * Updates the visual state of a custom dropdown based on its current value.
- */
-function updateCustomDropdownVisual(dropdownId, value) {
- const dropdown = document.getElementById(dropdownId);
- if (!dropdown) return;
-
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
- const options = dropdown.querySelectorAll(".custom-dropdown-option");
-
- options.forEach((opt) => {
- const optValue = opt.getAttribute("data-value");
- if (optValue === value) {
- opt.classList.add("selected");
- if (selectedText) selectedText.textContent = opt.textContent;
- } else {
- opt.classList.remove("selected");
- }
- });
-}
-
-function scrollTreeItemIntoView(element, alignToTop) {
- if (!element) return;
- const scrollContainer = document.getElementById("sidebar-panel-vaults");
- if (!scrollContainer) return;
-
- const containerRect = scrollContainer.getBoundingClientRect();
- const elementRect = element.getBoundingClientRect();
- const isAbove = elementRect.top < containerRect.top;
- const isBelow = elementRect.bottom > containerRect.bottom;
-
- if (!isAbove && !isBelow && !alignToTop) return;
-
- const currentTop = scrollContainer.scrollTop;
- const offsetTop = element.offsetTop;
- const shouldCenter = alignToTop === "center";
- const centeredTop = Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height / 2) + (elementRect.height / 2));
- const targetTop = shouldCenter
- ? centeredTop
- : alignToTop
- ? Math.max(0, offsetTop - 60)
- : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35);
-
- scrollContainer.scrollTo({
- top: targetTop,
- behavior: "smooth",
- });
-}
-
-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), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- safeCreateIcons();
-}
-
-async function focusVaultInSidebar(vaultName) {
- switchSidebarTab("vaults");
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (childContainer && childContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
- scrollTreeItemIntoView(vaultItem, false);
-}
-
-async function refreshTagsForContext() {
- const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
- const data = await api(`/api/tags${vaultParam}`);
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
-}
-
-// ---------------------------------------------------------------------------
-// Helper: Check if path should be displayed based on hideHiddenFiles setting
-// ---------------------------------------------------------------------------
-function shouldDisplayPath(path, vaultName) {
- // Get hideHiddenFiles setting for this vault (default: false = show all)
- const settings = vaultSettings[vaultName] || { hideHiddenFiles: false };
-
- if (!settings.hideHiddenFiles) {
- // Show all files
- return true;
- }
-
- // Check if any segment of the path starts with a dot (hidden)
- const segments = path.split("/").filter(Boolean);
- for (const segment of segments) {
- if (segment.startsWith(".")) {
- return false; // Hide this path
- }
- }
-
- return true; // Show this path
-}
-
-async function loadVaultSettings() {
- try {
- const settings = await api("/api/vaults/settings/all");
- vaultSettings = settings;
- } catch (err) {
- console.error("Failed to load vault settings:", err);
- vaultSettings = {};
- }
-}
-
-// ---------------------------------------------------------------------------
-// Sidebar — Vault tree
-// ---------------------------------------------------------------------------
-async function loadVaults() {
- const vaults = await api("/api/vaults");
- allVaults = vaults;
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- // Prepare dropdown options
- const dropdownOptions = [{ value: "all", text: t('help.all_vaults') }, ...vaults.map((v) => ({ value: v.name, text: v.name }))];
-
- // Populate custom dropdowns
- populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all");
- populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all");
-
- // Populate standard selects
- _populateRecentVaultFilter();
- if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.populateVaultFilter) {
- DashboardRecentWidget.populateVaultFilter();
- }
-
- vaults.forEach((v) => {
- // Sidebar tree entry
- const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
- vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
-
- vaultItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
- });
- attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
- attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
-
- container.appendChild(vaultItem);
-
- const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
- container.appendChild(childContainer);
- });
-
- syncVaultSelectors();
- safeCreateIcons();
-}
-
-/**
- * Refreshes the sidebar tree while preserving the expanded state of vaults and folders.
- * Optimized to avoid a full sidebar wipe and minimize visible loading states.
- */
-/**
- * Incrementally update a directory container without wiping existing DOM.
- * Only adds new items, removes deleted ones, and updates changed ones.
- */
-async function incrementalLoadDirectory(vaultName, dirPath, container) {
- let data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- // Server unavailable — keep existing content
- return;
- }
-
- // Build a map of existing DOM elements by path
- const existingItems = {};
- const existingChildren = {}; // path -> child container (for directories)
- for (let i = 0; i < container.children.length; i++) {
- const child = container.children[i];
- if (child.classList.contains("tree-item") && child.dataset.path) {
- existingItems[child.dataset.path] = child;
- // The next sibling should be the tree-children container for this directory
- if (i + 1 < container.children.length) {
- const next = container.children[i + 1];
- if (next.classList.contains("tree-children")) {
- existingChildren[child.dataset.path] = next;
- }
- }
- }
- }
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- if (!shouldDisplayPath(item.path, vaultName)) return;
-
- const existing = existingItems[item.path];
-
- if (existing) {
- // Item already exists — reuse it, but update text/badge if needed
- const textEl = existing.querySelector(".tree-item-text");
- const displayName = item.type === "file" && item.name.match(/\.md$/i)
- ? item.name.replace(/\.md$/i, "")
- : item.name;
- if (textEl && textEl.textContent !== displayName) {
- textEl.textContent = displayName;
- }
- // Update badge for directories
- if (item.type === "directory") {
- const badge = existing.querySelector(".badge-small");
- const newBadge = `(${item.children_count})`;
- if (badge && badge.textContent !== newBadge) {
- badge.textContent = newBadge;
- } else if (!badge) {
- existing.appendChild(smallBadge(item.children_count));
- }
- }
- fragment.appendChild(existing);
- // Also re-add the child container for directories
- if (item.type === "directory" && existingChildren[item.path]) {
- fragment.appendChild(existingChildren[item.path]);
- } else if (item.type === "directory") {
- // Directory existed but no child container — create one
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
- }
- } else {
- // New item — create it
- 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- TabManager.openPreview(vaultName, item.path);
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
- });
-
- fragment.appendChild(fileItem);
- }
- }
- });
-
- // Replace container content in a single batch operation to avoid flash
- container.textContent = "";
- container.appendChild(fragment);
-}
-
-async function refreshSidebarTreePreservingState() {
- // 1. Capture expanded states
- const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
- .filter((v) => {
- const children = document.getElementById(`vault-children-${v.dataset.vault}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((v) => v.dataset.vault);
-
- const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]"))
- .filter((item) => {
- const vault = item.dataset.vault;
- const path = item.dataset.path;
- const children = document.getElementById(`dir-${vault}-${path}`);
- return children && !children.classList.contains("collapsed");
- })
- .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path }));
-
- const selectedItem = document.querySelector(".tree-item.path-selected");
- const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
-
- // 2. Soft update: vault names/counts without wiping the tree
- try {
- const vaults = await api("/api/vaults");
- allVaults = vaults;
- vaults.forEach((v) => {
- const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`);
- if (vItem) {
- const badge = vItem.querySelector(".badge-small");
- if (badge) badge.textContent = `(${v.file_count})`;
- }
- });
- } catch (e) {
- console.warn("Soft vault refresh failed, falling back to full reload", e);
- await loadVaults();
- return;
- }
-
- // 3. Incrementally update expanded vaults (no DOM wipe)
- for (const vName of expandedVaults) {
- const container = document.getElementById(`vault-children-${vName}`);
- if (container) {
- await incrementalLoadDirectory(vName, "", container);
- }
- }
-
- // 4. Incrementally update expanded directories (parents first, no DOM wipe)
- expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
- for (const dir of expandedDirs) {
- const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
- if (container) {
- try {
- await incrementalLoadDirectory(dir.vault, dir.path, container);
- container.classList.remove("collapsed");
- const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
- if (dItem) {
- const chev = dItem.querySelector("[data-lucide]");
- if (chev) chev.setAttribute("data-lucide", "chevron-down");
- }
- } catch (e) {
- console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
- }
- }
- }
-
- // 5. Restore selection
- if (selectedState) {
- await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false });
- }
-
- safeCreateIcons();
-}
-
-async function toggleVault(itemEl, vaultName, forceExpand) {
- const childContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!childContainer) return;
-
- scrollTreeItemIntoView(itemEl, false);
-
- const shouldExpand = forceExpand || childContainer.classList.contains("collapsed");
-
- if (shouldExpand) {
- // Expand — load children if empty
- if (childContainer.children.length === 0) {
- await loadDirectory(vaultName, "", childContainer);
- }
- childContainer.classList.remove("collapsed");
- // Swap chevron
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- safeCreateIcons();
- } else {
- childContainer.classList.add("collapsed");
- const chevron = itemEl.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-right");
- safeCreateIcons();
- }
-}
-
-async function expandDirectoryInSidebar(vaultName, dirPath, dirItem) {
- const subContainer = document.getElementById(`dir-${vaultName}-${dirPath}`);
- if (!subContainer) return null;
-
- if (subContainer.children.length === 0) {
- await loadDirectory(vaultName, dirPath, subContainer);
- }
-
- subContainer.classList.remove("collapsed");
- if (dirItem) {
- const chevron = dirItem.querySelector("[data-lucide]");
- if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
- }
- safeCreateIcons();
- return subContainer;
-}
-
-async function focusPathInSidebar(vaultName, targetPath, options) {
- switchSidebarTab("vaults");
-
- const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
- if (!vaultItem) return;
-
- document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
- vaultItem.classList.add("focused");
-
- const vaultContainer = document.getElementById(`vault-children-${vaultName}`);
- if (!vaultContainer) return;
-
- if (vaultContainer.classList.contains("collapsed")) {
- await toggleVault(vaultItem, vaultName, true);
- }
-
- if (!targetPath) {
- // Clear any previous path selection
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- scrollTreeItemIntoView(vaultItem, options && options.alignToTop);
- return;
- }
-
- const segments = targetPath.split("/").filter(Boolean);
- let currentContainer = vaultContainer;
- let cumulativePath = "";
- let lastTargetItem = null;
-
- for (let index = 0; index < segments.length; index++) {
- cumulativePath += (cumulativePath ? "/" : "") + segments[index];
-
- let targetItem = null;
- try {
- targetItem = currentContainer.querySelector(`.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(cumulativePath)}"]`);
- } catch (e) {
- targetItem = null;
- }
-
- if (!targetItem) {
- return;
- }
-
- lastTargetItem = targetItem;
-
- const isLastSegment = index === segments.length - 1;
- if (!isLastSegment) {
- const nextContainer = await expandDirectoryInSidebar(vaultName, cumulativePath, targetItem);
- if (nextContainer) {
- currentContainer = nextContainer;
- }
- }
- }
-
- if (lastTargetItem && options && options.expandTarget) {
- await expandDirectoryInSidebar(vaultName, targetPath, lastTargetItem);
- }
-
- // Clear previous path selections and highlight the final target
- document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
- if (lastTargetItem) {
- lastTargetItem.classList.add("path-selected");
- }
-
- scrollTreeItemIntoView(lastTargetItem, options && options.alignToTop);
-}
-
-function getParentDirectoryPath(filePath) {
- if (!filePath) return "";
- const segments = filePath.split("/").filter(Boolean);
- if (segments.length <= 1) return "";
- segments.pop();
- return segments.join("/");
-}
-
-function syncActiveFileTreeItem(vaultName, filePath) {
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
- if (!vaultName || !filePath) return;
- const selector = `.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(filePath)}"]`;
- try {
- const active = document.querySelector(selector);
- if (active) active.classList.add("active");
- } catch (e) {
- /* selector might fail on special chars */
- }
-}
-
-async function loadDirectory(vaultName, dirPath, container) {
- // Only show the loading spinner if the container is currently empty
- const isEmpty = container.children.length === 0;
- if (isEmpty) {
- container.innerHTML = '';
- }
-
- var data;
- try {
- const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
- data = await api(url);
- } catch (err) {
- container.innerHTML = 'Erreur de chargement
';
- return;
- }
- container.innerHTML = "";
-
- const fragment = document.createDocumentFragment();
-
- data.items.forEach((item) => {
- // Apply client-side filtering for hidden files
- if (!shouldDisplayPath(item.path, vaultName)) {
- return; // Skip this 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), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
- attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
- attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
- fragment.appendChild(dirItem);
-
- const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
- fragment.appendChild(subContainer);
-
- dirItem.addEventListener("click", async () => {
- scrollTreeItemIntoView(dirItem, false);
- 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();
- }
- });
-
- dirItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly);
- });
- } else {
- const fileIconName = getFileIcon(item.name);
- const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
- const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
- attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
- attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
- fileItem.addEventListener("click", () => {
- scrollTreeItemIntoView(fileItem, false);
- TabManager.openPreview(vaultName, item.path);
- closeMobileSidebar();
- });
-
- fileItem.addEventListener("dblclick", (e) => {
- e.preventDefault();
- TabManager.openPersistent(vaultName, item.path);
- });
-
- fileItem.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- const isReadonly = false;
- ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly);
- });
-
- fragment.appendChild(fileItem);
- }
- });
-
- container.appendChild(fragment);
- safeCreateIcons();
-}
-
-// ---------------------------------------------------------------------------
-// Sidebar filter
-// ---------------------------------------------------------------------------
-function initSidebarFilter() {
- const input = document.getElementById("sidebar-filter-input");
- const caseBtn = document.getElementById("sidebar-filter-case-btn");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
-
- input.addEventListener("input", () => {
- const hasText = input.value.length > 0;
- clearBtn.style.display = hasText ? "flex" : "none";
- clearTimeout(filterDebounce);
- filterDebounce = setTimeout(async () => {
- const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (hasText) {
- if (activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- } else {
- if (activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- }
- }, 220);
- });
-
- caseBtn.addEventListener("click", async () => {
- sidebarFilterCaseSensitive = !sidebarFilterCaseSensitive;
- caseBtn.classList.toggle("active");
- const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
- if (input.value.trim()) {
- if (activeSidebarTab === "vaults") {
- await performTreeSearch(q);
- } else {
- filterTagCloud(q);
- }
- }
- });
-
- clearBtn.addEventListener("click", async () => {
- input.value = "";
- clearBtn.style.display = "none";
- sidebarFilterCaseSensitive = false;
- caseBtn.classList.remove("active");
- clearTimeout(filterDebounce);
- if (activeSidebarTab === "vaults") {
- await restoreSidebarTree();
- } else {
- filterTagCloud("");
- }
- });
-
- clearBtn.style.display = "none";
-}
-
-async function performTreeSearch(query) {
- if (!query) {
- await restoreSidebarTree();
- return;
- }
-
- try {
- const vaultParam = selectedContextVault === "all" ? "all" : selectedContextVault;
- const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`;
- const data = await api(url);
- renderFilteredSidebarResults(query, data.results);
- } catch (err) {
- console.error("Tree search error:", err);
- renderFilteredSidebarResults(query, []);
- }
-}
-
-async function restoreSidebarTree() {
- await refreshSidebarForContext();
- if (currentVault) {
- focusPathInSidebar(currentVault, currentPath || "", { alignToTop: false }).catch(() => {});
- }
-}
-
-function renderFilteredSidebarResults(query, results) {
- const container = document.getElementById("vault-tree");
- container.innerHTML = "";
-
- const grouped = new Map();
- results.forEach((result) => {
- if (!grouped.has(result.vault)) {
- grouped.set(result.vault, []);
- }
- grouped.get(result.vault).push(result);
- });
-
- if (grouped.size === 0) {
- container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode(t("sidebar.no_results"))]));
- return;
- }
-
- grouped.forEach((entries, vaultName) => {
- entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" }));
-
- const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]);
- container.appendChild(vaultHeader);
-
- const resultsWrapper = el("div", { class: "filter-results-group" });
- entries.forEach((entry) => {
- const resultItem = el(
- "div",
- {
- class: `tree-item filter-result-item filter-result-${entry.type}`,
- "data-vault": entry.vault,
- "data-path": entry.path,
- "data-type": entry.type,
- },
- [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)],
- );
-
- const textWrap = el("div", { class: "filter-result-text" });
- const primary = el("div", { class: "filter-result-primary" });
- appendHighlightedText(primary, entry.name, query, sidebarFilterCaseSensitive);
- const secondary = el("div", { class: "filter-result-secondary" });
- appendHighlightedText(secondary, entry.path, query, sidebarFilterCaseSensitive);
- textWrap.appendChild(primary);
- textWrap.appendChild(secondary);
- resultItem.appendChild(textWrap);
-
- resultItem.addEventListener("click", async () => {
- const input = document.getElementById("sidebar-filter-input");
- const clearBtn = document.getElementById("sidebar-filter-clear-btn");
- if (input) input.value = "";
- if (clearBtn) clearBtn.style.display = "none";
- await restoreSidebarTree();
- if (entry.type === "directory") {
- await focusPathInSidebar(entry.vault, entry.path, { alignToTop: true, expandTarget: true });
- } else {
- await TabManager.openPreview(entry.vault, entry.path);
- await focusPathInSidebar(entry.vault, getParentDirectoryPath(entry.path), { alignToTop: true, expandTarget: true });
- syncActiveFileTreeItem(entry.vault, entry.path);
- }
- closeMobileSidebar();
- });
-
- resultsWrapper.appendChild(resultItem);
- });
-
- container.appendChild(resultsWrapper);
- });
-
- flushIcons();
-}
-
-function filterSidebarTree(query) {
- const tree = document.getElementById("vault-tree");
- const items = tree.querySelectorAll(".tree-item");
- const containers = tree.querySelectorAll(".tree-children");
-
- if (!query) {
- items.forEach((item) => item.classList.remove("filtered-out"));
- containers.forEach((c) => {
- c.classList.remove("filtered-out");
- // Keep current collapsed state when clearing filter
- });
- return;
- }
-
- // First pass: mark all as filtered out
- items.forEach((item) => item.classList.add("filtered-out"));
- containers.forEach((c) => c.classList.add("filtered-out"));
-
- // Second pass: find matching items and mark them + ancestors + descendants
- const matchingItems = new Set();
-
- items.forEach((item) => {
- const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase();
- const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (text.includes(searchQuery)) {
- matchingItems.add(item);
- 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;
- }
-
- // If this is a directory (has a children container after it), show all descendants
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- // Recursively show all children in this container
- showAllDescendants(nextEl);
- }
- }
- });
-
- // Third pass: show items that are descendants of matching directories
- // and ensure their containers are visible
- matchingItems.forEach((item) => {
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- const children = nextEl.querySelectorAll(".tree-item");
- children.forEach((child) => child.classList.remove("filtered-out"));
- }
- });
-}
-
-function showAllDescendants(container) {
- const items = container.querySelectorAll(".tree-item");
- items.forEach((item) => {
- item.classList.remove("filtered-out");
- // If this item has children, also show them
- const nextEl = item.nextElementSibling;
- if (nextEl && nextEl.classList.contains("tree-children")) {
- nextEl.classList.remove("filtered-out");
- nextEl.classList.remove("collapsed");
- }
- });
- // Also ensure all nested containers are visible
- const nestedContainers = container.querySelectorAll(".tree-children");
- nestedContainers.forEach((c) => {
- c.classList.remove("filtered-out");
- c.classList.remove("collapsed");
- });
-}
-
-function filterTagCloud(query) {
- const tags = document.querySelectorAll("#tag-cloud .tag-item");
- tags.forEach((tag) => {
- const text = sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase();
- const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
- if (!query || text.includes(searchQuery)) {
- tag.classList.remove("filtered-out");
- } else {
- tag.classList.add("filtered-out");
- }
- });
-}
-
-// ---------------------------------------------------------------------------
-// Tag Filter Service
-// ---------------------------------------------------------------------------
-const TagFilterService = {
- defaultFilters: [
- { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true },
- { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true },
- { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true },
- ],
-
- getConfig() {
- const stored = localStorage.getItem("obsigate-tag-filters");
- if (stored) {
- try {
- return JSON.parse(stored);
- } catch (e) {
- return { tagFilters: this.defaultFilters };
- }
- }
- return { tagFilters: this.defaultFilters };
- },
-
- saveConfig(config) {
- localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
- },
-
- patternToRegex(pattern) {
- // 1. Escape ALL special regex characters
- // We use a broader set including * and .
- let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-
- // 2. Convert escaped '*' to '.*' (wildcard)
- regex = regex.replace(/\\\*/g, ".*");
-
- // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*'
- // We also handle optional whitespace around it to make it more user-friendly
- regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*");
-
- return regex;
- },
-
- isTagFiltered(tag) {
- const config = this.getConfig();
- const filters = config.tagFilters || this.defaultFilters;
- const tagWithHash = `#${tag}`;
-
- for (const filter of filters) {
- if (!filter.enabled) continue;
- try {
- // Robustly handle regex with or without ^/$
- let patternStr = filter.regex;
- if (!patternStr.startsWith("^")) patternStr = "^" + patternStr;
- if (!patternStr.endsWith("$")) patternStr = patternStr + "$";
-
- const regex = new RegExp(patternStr);
- if (regex.test(tagWithHash)) {
- return true;
- }
- } catch (e) {
- console.warn("Invalid regex:", filter.regex, e);
- }
- }
- return false;
- },
-
- filterTags(tags) {
- const filtered = {};
- Object.entries(tags).forEach(([tag, count]) => {
- if (!this.isTagFiltered(tag)) {
- filtered[tag] = count;
- }
- });
- return filtered;
- },
-};
-
-// ---------------------------------------------------------------------------
-// Tags
-// ---------------------------------------------------------------------------
-async function loadTags() {
- const data = await api("/api/tags");
- const filteredTags = TagFilterService.filterTags(data.tags);
- renderTagCloud(filteredTags);
-}
-
-function renderTagCloud(tags) {
- const cloud = document.getElementById("tag-cloud");
- cloud.innerHTML = "";
-
- const counts = Object.values(tags);
- if (counts.length === 0) return;
-
- const maxCount = Math.max(...counts);
- const minSize = 0.7;
- const maxSize = 1.25;
-
- Object.entries(tags).forEach(([tag, count]) => {
- const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0;
- const size = minSize + ratio * (maxSize - minSize);
- const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]);
- tagEl.addEventListener("click", () => searchByTag(tag));
- cloud.appendChild(tagEl);
- });
-}
-
-function addTagFilter(tag) {
- if (!selectedTags.includes(tag)) {
- selectedTags.push(tag);
- performTagSearch();
- }
-}
-
-function removeTagFilter(tag) {
- selectedTags = selectedTags.filter((t) => t !== tag);
- if (selectedTags.length > 0) {
- performTagSearch();
- } else {
- const input = document.getElementById("search-input");
- if (input.value.trim()) {
- performAdvancedSearch(input.value.trim(), document.getElementById("vault-filter").value, null);
- } else {
- showWelcome();
- }
- }
-}
-
-function performTagSearch() {
- const input = document.getElementById("search-input");
- const query = input.value.trim();
- const vault = document.getElementById("vault-filter").value;
- performAdvancedSearch(query, vault, selectedTags.length > 0 ? selectedTags.join(",") : null);
-}
-
-function buildSearchResultsHeader(data, query, tagFilter) {
- const header = el("div", { class: "search-results-header" });
- const summaryText = el("span", { class: "search-results-summary-text" });
-
- if (query && tagFilter) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`;
- } else if (query) {
- summaryText.textContent = `${data.count} résultat(s) pour "${query}"`;
- } else if (tagFilter) {
- summaryText.textContent = `${data.count} fichier(s) avec les tags`;
- } else {
- summaryText.textContent = `${data.count} résultat(s)`;
- }
-
- header.appendChild(summaryText);
-
- if (selectedTags.length > 0) {
- const activeTags = el("div", { class: "search-results-active-tags" });
- selectedTags.forEach((tag) => {
- const removeBtn = el(
- "button",
- {
- class: "search-results-active-tag-remove",
- title: `Retirer ${tag} du filtre`,
- "aria-label": `Retirer ${tag} du filtre`,
- },
- [document.createTextNode("×")],
- );
- removeBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- removeTagFilter(tag);
- });
-
- const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
- activeTags.appendChild(chip);
- });
- header.appendChild(activeTags);
- }
-
- return header;
-}
-
-function searchByTag(tag) {
- addTagFilter(tag);
-}
-
diff --git a/desktop/frontend-static/js/state.js b/desktop/frontend-static/js/state.js
deleted file mode 100644
index e5d290c..0000000
--- a/desktop/frontend-static/js/state.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/* ObsiGate — Shared mutable application state.
- Use state.xxx to read/write any value. ES module imports are read-only,
- so we export a single mutable object instead of individual let bindings. */
-export const state = {
- APP_VERSION: "1.5.0",
-
- // Core navigation
- currentVault: null,
- currentPath: null,
- allVaults: [],
- selectedContextVault: "all",
-
- // Search
- searchTimeout: null,
- searchAbortController: null,
- advancedSearchOffset: 0,
- advancedSearchTotal: 0,
- advancedSearchSort: "relevance",
- advancedSearchLastQuery: "",
- suggestAbortController: null,
- dropdownActiveIndex: -1,
- dropdownItems: [],
- currentSearchId: 0,
- selectedTags: [],
- searchCaseSensitive: false,
- searchWholeWord: false,
- searchRegex: false,
- searchFilterVisible: false,
-
- // Search constants
- SEARCH_HISTORY_KEY: "obsigate_search_history",
- MAX_HISTORY_ENTRIES: 50,
- SUGGEST_DEBOUNCE_MS: 150,
- ADVANCED_SEARCH_LIMIT: 50,
- MIN_SEARCH_LENGTH: 2,
- SEARCH_TIMEOUT_MS: 30000,
-
- // Viewer state
- showingSource: false,
- cachedRawSource: null,
- editorView: null,
- editorVault: null,
- editorPath: null,
- fallbackEditorEl: null,
- _iconDebounceTimer: null,
-
- // Outline/TOC
- outlineObserver: null,
- activeHeadingId: null,
- headingsCache: [],
- rightSidebarVisible: true,
- rightSidebarWidth: 280,
-
- // Sidebar
- sidebarFilterCaseSensitive: false,
- activeSidebarTab: "vaults",
- filterDebounce: null,
- vaultSettings: {},
-};
\ No newline at end of file
diff --git a/desktop/frontend-static/js/sync.js b/desktop/frontend-static/js/sync.js
deleted file mode 100644
index f0e6bc9..0000000
--- a/desktop/frontend-static/js/sync.js
+++ /dev/null
@@ -1,643 +0,0 @@
-/* ObsiGate — Sync: SSE client + PWA registration + Offline mode */
-import { state } from './state.js';
-import { t } from './i18n.js';
-import { showToast } from './ui.js';
-import { api, AuthManager } from './auth.js';
-import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js';
-import { loadRecentFiles, initAboutModal } from './config.js';
-import { openFile } from './viewer.js';
-import { initOffline, syncFileIndexFromServer } from './offline.js';
-import { initThemes } from './themes.js';
-
-// ---------------------------------------------------------------------------
-// SSE Client — IndexUpdateManager
-// ---------------------------------------------------------------------------
-export const IndexUpdateManager = (() => {
- let eventSource = null;
- let reconnectTimer = null;
- let reconnectDelay = 1000;
- const MAX_RECONNECT_DELAY = 30000;
- let recentEvents = [];
- const MAX_RECENT_EVENTS = 20;
- let connectionState = "disconnected"; // disconnected | connecting | connected
-
- function connect() {
- if (eventSource) {
- eventSource.close();
- }
- connectionState = "connecting";
- _updateBadge();
-
- eventSource = new EventSource("/api/events");
-
- eventSource.addEventListener("connected", (e) => {
- connectionState = "connected";
- reconnectDelay = 1000;
- _updateBadge();
- });
-
- eventSource.addEventListener("index_updated", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_updated", data);
- _onIndexUpdated(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_reloaded", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_reloaded", data);
- _onIndexReloaded(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_added", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_added", data);
- showToast(t('sync.vault_added', { vault: data.vault, count: data.stats.file_count }), "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_removed", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_removed", data);
- showToast(t('sync.vault_removed', { vault: data.vault }), "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_start", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_start", data);
- connectionState = "syncing";
- _updateBadge();
- showToast(t('sync.index_started', { count: data.total_vaults }), "info");
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_progress", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_progress", data);
- connectionState = "syncing";
- _updateBadge();
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_complete", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_complete", data);
- connectionState = "connected";
- _updateBadge();
- showToast(t('sync.index_complete', { count: data.total_files }), "success");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.onerror = () => {
- connectionState = "disconnected";
- _updateBadge();
- eventSource.close();
- eventSource = null;
- _scheduleReconnect();
- };
- }
-
- function _scheduleReconnect() {
- if (reconnectTimer) clearTimeout(reconnectTimer);
- reconnectTimer = setTimeout(() => {
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
- connect();
- }, reconnectDelay);
- }
-
- function _addEvent(type, data) {
- recentEvents.unshift({
- type,
- data,
- timestamp: new Date().toISOString(),
- });
- if (recentEvents.length > MAX_RECENT_EVENTS) {
- recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
- }
- }
-
- async function _onIndexUpdated(data) {
- // Brief syncing state
- connectionState = "syncing";
- _updateBadge();
-
- const n = data.total_changes || 0;
- const vaults = (data.vaults || []).join(", ");
- // Toast removed: silent auto-indexing — no notification needed
-
- // Refresh sidebar and tags if affected vault matches current context
- const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
- if (affectsCurrentVault) {
- try {
- await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
- // Refresh current file if it was updated
- if (state.currentVault && state.currentPath) {
- const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath);
- if (changed) {
- openFile(state.currentVault, state.currentPath);
- }
- }
- } catch (err) {
- console.error("Error refreshing after index update:", err);
- }
- }
-
- // Refresh recent tab if it is active
- if (state.activeSidebarTab === "recent") {
- const vaultFilter = document.getElementById("recent-vault-filter");
- loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
- }
-
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- async function _onIndexReloaded(data) {
- connectionState = "syncing";
- _updateBadge();
- showToast(t("sync.index_reloaded"), "info");
- try {
- await Promise.all([loadVaults(), loadTags()]);
- } catch (err) {
- console.error("Error refreshing after full reload:", err);
- }
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- function _updateBadge() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
- badge.className = "sync-badge sync-badge--" + connectionState;
- const labels = {
- disconnected: t('sync.disconnected'),
- connecting: "Connexion...",
- connected: t('sync.connected'),
- syncing: t('sync.updating'),
- };
- badge.title = labels[connectionState] || connectionState;
- }
-
- function disconnect() {
- if (eventSource) {
- eventSource.close();
- eventSource = null;
- }
- if (reconnectTimer) {
- clearTimeout(reconnectTimer);
- reconnectTimer = null;
- }
- connectionState = "disconnected";
- _updateBadge();
- }
-
- function getState() {
- return connectionState;
- }
-
- function getRecentEvents() {
- return recentEvents;
- }
-
- return { connect, disconnect, getState, getRecentEvents };
-})();
-
-// ---------------------------------------------------------------------------
-// Sync status badge and panel
-// ---------------------------------------------------------------------------
-
-function initSyncStatus() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
-
- badge.addEventListener("click", (e) => {
- e.stopPropagation();
- toggleSyncPanel();
- });
-
- IndexUpdateManager.connect();
-
- // Initialize AI status tracking
- _initAIStatus();
-}
-
-function toggleSyncPanel() {
- let panel = document.getElementById("sync-panel");
- if (panel) {
- panel.remove();
- return;
- }
- // Auto reconnect if disconnected when user opens the panel
- if (IndexUpdateManager.getState() === "disconnected") {
- IndexUpdateManager.connect();
- }
- panel = document.createElement("div");
- panel.id = "sync-panel";
- panel.className = "sync-panel";
- _renderSyncPanel(panel);
- document.body.appendChild(panel);
-
- // Close on outside click
- setTimeout(() => {
- document.addEventListener("click", _closeSyncPanelOutside, { once: true });
- }, 0);
-}
-
-function _closeSyncPanelOutside(e) {
- const panel = document.getElementById("sync-panel");
- if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
- panel.remove();
- }
-}
-
-function _renderSyncPanel(panel) {
- const state = IndexUpdateManager.getState();
- const events = IndexUpdateManager.getRecentEvents();
-
- const stateLabels = {
- disconnected: t('sync.disconnected'),
- connecting: "Connexion...",
- connected: t('sync.connected_short'),
- syncing: "Synchronisation...",
- };
-
- let html = ``;
-
- if (events.length === 0) {
- html += `${t('sync.no_recent_events')}
`;
- } else {
- html += ``;
- events.slice(0, 10).forEach((ev) => {
- const time = new Date(ev.timestamp).toLocaleTimeString();
- const typeLabels = {
- index_updated: t('sync.event_index_updated'),
- index_reloaded: "Rechargement",
- vault_added: t('sync.event_vault_added'),
- vault_removed: t('sync.event_vault_removed'),
- index_start: t('sync.event_index_start'),
- index_progress: t('sync.event_index_progress'),
- index_complete: "Indexation tech.",
- };
- const label = typeLabels[ev.type] || ev.type;
- let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
- if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
- if (ev.type === "index_progress") detail = t("sync.vault_files", {vault: ev.data.vault, files: ev.data.files});
- if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
- html += `
- ${label}
- ${detail}
- ${time}
-
`;
- });
- html += `
`;
- }
-
- panel.innerHTML = html;
-
- // ── AI Status section ──
- _renderAIStatusSection(panel);
-}
-
-// ---------------------------------------------------------------------------
-// AI Status Indicator (inside sync panel + header badge animation)
-// ---------------------------------------------------------------------------
-
-let _aiProcessing = false;
-let _aiConfigured = null; // null = unknown, true/false
-let _aiAutocomplete = null; // null = unknown, {available, server_ok, model_loaded, model, error}
-
-/** Check if AI is configured by calling the /api/ai/status endpoint.
- * Updates _aiConfigured, _aiAutocomplete and refreshes the panel if open. */
-async function _checkAIStatus() {
- try {
- const status = await api('/api/ai/status');
- _aiConfigured = status.configured === true;
- _aiAutocomplete = status.autocomplete || null;
- } catch {
- _aiConfigured = false;
- _aiAutocomplete = null;
- }
- _updateAIStatusInPanel();
-}
-
-function _renderAIStatusSection(panel) {
- const section = document.createElement('div');
- section.className = 'sync-panel__ai-section';
- section.id = 'sync-panel-ai';
-
- // ---- External provider status ----
- let extIcon, extText, extClass;
- if (_aiProcessing) {
- extIcon = 'loader';
- extText = t('sync.processing');
- extClass = 'sync-panel__ai--processing';
- } else if (_aiConfigured === true) {
- extIcon = 'check-circle';
- extText = 'Fournisseur externe actif';
- extClass = 'sync-panel__ai--active';
- } else if (_aiConfigured === false) {
- extIcon = 'x-circle';
- extText = t('sync.no_provider');
- extClass = 'sync-panel__ai--inactive';
- } else {
- extIcon = 'help-circle';
- extText = t('sync.verifying');
- extClass = 'sync-panel__ai--unknown';
- }
-
- // ---- Autocomplete (Ollama) status ----
- let acIcon, acText, acClass;
- if (_aiAutocomplete && _aiAutocomplete.available) {
- acIcon = 'check-circle';
- acText = t('sync.autocomplete_active');
- acClass = 'sync-panel__ai--active';
- } else if (_aiAutocomplete && _aiAutocomplete.server_ok && !_aiAutocomplete.model_loaded) {
- acIcon = 'alert-triangle';
- acText = t('sync.model_not_loaded');
- acClass = 'sync-panel__ai--inactive';
- } else if (_aiAutocomplete && _aiAutocomplete.error) {
- acIcon = 'x-circle';
- acText = 'Serveur Ollama injoignable';
- acClass = 'sync-panel__ai--inactive';
- } else if (_aiAutocomplete === null) {
- acIcon = 'help-circle';
- acText = t('sync.verifying');
- acClass = 'sync-panel__ai--unknown';
- } else {
- acIcon = 'x-circle';
- acText = t('sync.not_configured');
- acClass = 'sync-panel__ai--inactive';
- }
-
- section.innerHTML = '' +
- 'Intelligence Artificielle
' +
- '' +
- 'Fournisseur ' +
- '' +
- ' ' +
- extText +
- ' ' +
- '
' +
- '' +
- '' + t('sync.autocomplete') + ' ' +
- '' +
- ' ' +
- acText +
- ' ' +
- '
';
-
- panel.appendChild(section);
- // Trigger icon refresh via global
- if (window._obsigateRefreshIcons) window._obsigateRefreshIcons();
-}
-
-/** Update the AI section in the open sync panel. */
-function _updateAIStatusInPanel() {
- const section = document.getElementById('sync-panel-ai');
- if (!section) return;
- section.innerHTML = '';
- const panel = document.getElementById('sync-panel');
- if (panel) {
- // Re-render just the AI section
- const oldSection = panel.querySelector('.sync-panel__ai-section');
- if (oldSection) oldSection.remove();
- _renderAIStatusSection(panel);
- }
-}
-
-/** Flash the sync badge white during AI processing.
- * NOTE: now handled via CSS class on #sync-badge.
- * The visual flash in the editor itself is handled by ai.js
- * which adds a processing indicator inline. */
-function _setBadgeAIProcessing(active) {
- const badge = document.getElementById('sync-badge');
- if (!badge) return;
- if (active) {
- badge.classList.add('sync-badge--ai-processing');
- } else {
- badge.classList.remove('sync-badge--ai-processing');
- }
-}
-
-/** Initialize AI status tracking. */
-function _initAIStatus() {
- // Check AI status immediately at startup
- _checkAIStatus();
-
- // Listen for AI processing events from ai.js
- document.addEventListener('ai-processing-start', () => {
- _aiProcessing = true;
- _setBadgeAIProcessing(true);
- _updateAIStatusInPanel();
- });
-
- document.addEventListener('ai-processing-end', () => {
- _aiProcessing = false;
- _setBadgeAIProcessing(false);
- _updateAIStatusInPanel();
- });
-
- // Re-check AI status when sync panel opens (in case config changed)
- const badge = document.getElementById('sync-badge');
- if (badge) {
- badge.addEventListener('click', () => {
- _checkAIStatus();
- }, { once: false });
- }
-}
-
-// ---------------------------------------------------------------------------
-// PWA Service Worker Registration
-// ---------------------------------------------------------------------------
-function registerServiceWorker() {
- if ("serviceWorker" in navigator) {
- window.addEventListener("load", () => {
- navigator.serviceWorker
- .register("/sw.js")
- .then((registration) => {
- console.log("[PWA] Service Worker registered successfully:", registration.scope);
-
- // Check for updates periodically
- setInterval(() => {
- registration.update();
- }, 60000); // Check every minute
-
- // Handle service worker updates
- registration.addEventListener("updatefound", () => {
- const newWorker = registration.installing;
- newWorker.addEventListener("statechange", () => {
- if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
- // New service worker available
- showUpdateNotification();
- }
- });
- });
- })
- .catch((error) => {
- console.log("[PWA] Service Worker registration failed:", error);
- });
- });
- }
-}
-
-function showUpdateNotification() {
- const message = document.createElement("div");
- message.className = "pwa-update-notification";
- message.innerHTML = `
-
- Une nouvelle version d'ObsiGate est disponible !
- Mettre à jour
- ×
-
- `;
- document.body.appendChild(message);
-
- // Auto-dismiss after 30 seconds
- setTimeout(() => {
- if (message.parentElement) {
- message.remove();
- }
- }, 30000);
-}
-
-// Handle install prompt
-let deferredPrompt;
-window.addEventListener("beforeinstallprompt", (e) => {
- e.preventDefault();
- deferredPrompt = e;
-
- // Show install button if desired
- const installBtn = document.getElementById("pwa-install-btn");
- if (installBtn) {
- installBtn.style.display = "block";
- installBtn.addEventListener("click", async () => {
- if (deferredPrompt) {
- deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
- console.log(`[PWA] User response to install prompt: ${outcome}`);
- deferredPrompt = null;
- installBtn.style.display = "none";
- }
- });
- }
-});
-
-// Log when app is installed
-window.addEventListener("appinstalled", () => {
- console.log("[PWA] ObsiGate has been installed");
- showToast(t("sync.install_success"), "success");
-});
-
-// ── Dashboard tab switching (runs on page load and after rebuild) ──
-function initDashboardTabs() {
- document.querySelectorAll(".dashboard-tab").forEach(tab => {
- // Remove existing listeners by cloning
- const newTab = tab.cloneNode(true);
- tab.parentNode.replaceChild(newTab, tab);
- newTab.addEventListener("click", function() {
- document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
- document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
- this.classList.add("active");
- const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
- if (panel) panel.classList.add("active");
- });
- });
-}
-
-// ---------------------------------------------------------------------------
-// Init — called by app.js orchestrator
-// ---------------------------------------------------------------------------
-export { initSyncStatus, initDashboardTabs };
-export function init() {
- registerServiceWorker();
- initOffline();
- initThemes();
- initAboutModal();
- // Sync file index after a short delay (let vaults load first).
- // Gate on auth at execution time (not scheduling time) so that
- // initAuth() has a chance to complete and set _authEnabled / token.
- // The login handler triggers syncFileIndexFromServer explicitly on
- // fresh login, so we only do it here for already-authenticated or
- // auth-disabled sessions.
- setTimeout(() => {
- if (!AuthManager._authEnabled || AuthManager.getToken()) {
- syncFileIndexFromServer();
- }
- }, 3000);
- initDashboardTabs();
-
- // Listen for Forge editor rename events
- window.addEventListener('message', function(e) {
- if (!e.data || !e.data.type) return;
- if (e.data.type === 'forge-renamed') {
- var vault = e.data.vault;
- var oldPath = e.data.oldPath;
- var newPath = e.data.newPath;
- console.log('[Forge] File renamed:', vault, oldPath, '->', newPath);
- var titleEl = document.getElementById('editor-title');
- if (titleEl && titleEl.textContent.indexOf('Forge:') === 0) {
- titleEl.textContent = 'Forge: ' + newPath.split('/').pop();
- }
- refreshSidebarTreePreservingState(vault);
- if (state.currentVault === vault && state.currentPath === oldPath) {
- import('./viewer.js').then(function(m) { m.openFile(vault, newPath); });
- }
- }
- if (e.data.type === 'forge-close') {
- var modal = document.getElementById('editor-modal');
- var forgeFrame = document.getElementById('forge-iframe');
- if (forgeFrame) forgeFrame.remove();
- if (modal) modal.classList.remove('active');
- // Restore brand text
- var brand = modal.querySelector('.editor-brand');
- if (brand) brand.textContent = 'ObsiGate';
- // Restore header/footer
- var headerEl = modal.querySelector('.editor-header');
- var footerEl = modal.querySelector('.editor-footer');
- if (headerEl) headerEl.style.display = '';
- if (footerEl) footerEl.style.display = '';
- // Refresh current file content
- if (state.currentVault && state.currentPath) {
- import('./viewer.js').then(function(m) { m.openFile(state.currentVault, state.currentPath); });
- }
- }
- });
-}
diff --git a/desktop/frontend-static/js/themes.js b/desktop/frontend-static/js/themes.js
deleted file mode 100644
index 42ea30b..0000000
--- a/desktop/frontend-static/js/themes.js
+++ /dev/null
@@ -1,566 +0,0 @@
-/* ObsiGate — Theme System
- * 15 themes x 2 modes (dark/light)
- * Persisted in localStorage as 'obsigate-theme'
- * Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js'
- */
-
-// ── Theme definitions ────────────────────────────────────────────────
-// Each theme has 'dark' and 'light' variants.
-// CSS variables are applied to :root via setProperty.
-
-var THEMES = {
-
- 'defaut-obsigate': {
- name: 'Defaut ObsiGate',
- desc: 'Theme original',
- modes: {
- 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',
- '--resize-handle': '#30363d', '--overlay-bg': 'rgba(0,0,0,0.5)',
- '--danger': '#ff7b72', '--danger-bg': '#3d1a18', '--success': '#3fb950', '--success-bg': '#1a3d1f',
- '--accent-card': '#58a6ff', '--accent-bg': '#1f2a3a', '--accent-text': '#79c0ff',
- '--green': '#3fb950', '--green-bg': '#1a3d1f', '--purple': '#a371f7', '--purple-bg': '#2d1b4e',
- '--surface': '#161b22', '--surface2': '#0f1117', '--surface3': '#0d0f14',
- '--border-md': '#30363d', '--text': '#e6edf3', '--text-2': '#8b949e', '--text-3': '#484f58',
- '--ok': '#34d399', '--warn': '#fbbf24', '--ai': '#8b5cf6', '--err': '#f87171'
- },
- 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',
- '--resize-handle': '#d0d7de', '--overlay-bg': 'rgba(0,0,0,0.3)',
- '--danger': '#cf222e', '--danger-bg': '#ffebe9', '--success': '#1a7f37', '--success-bg': '#dafbe1',
- '--accent-card': '#0969da', '--accent-bg': '#ddf4ff', '--accent-text': '#0969da',
- '--green': '#1a7f37', '--green-bg': '#dafbe1', '--purple': '#8250df', '--purple-bg': '#fbefff',
- '--surface': '#f6f8fa', '--surface2': '#ffffff', '--surface3': '#f0f2f5',
- '--border-md': '#d0d7de', '--text': '#1f2328', '--text-2': '#57606a', '--text-3': '#9198a1',
- '--ok': '#1a7f37', '--warn': '#9a6700', '--ai': '#8250df', '--err': '#cf222e'
- }
- }
- },
-
- 'github': {
- name: 'GitHub', desc: 'Contraste doux',
- modes: {
- dark: {
- '--bg-primary': '#0d1117', '--bg-secondary': '#161b22', '--bg-sidebar': '#0d1117',
- '--bg-hover': '#1c2128', '--border': '#30363d', '--text-primary': '#f0f6fc',
- '--text-secondary': '#8b949e', '--text-muted': '#6e7681', '--accent': '#58a6ff',
- '--accent-green': '#3fb950', '--tag-bg': '#13233a', '--tag-text': '#58a6ff',
- '--code-bg': '#161b22', '--search-bg': '#0d1117', '--scrollbar': '#30363d',
- '--resize-handle': '#30363d', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#f85149', '--danger-bg': '#490202', '--success': '#3fb950', '--success-bg': '#04260f',
- '--accent-card': '#58a6ff', '--accent-bg': '#13233a', '--accent-text': '#58a6ff',
- '--green': '#3fb950', '--green-bg': '#04260f', '--purple': '#a371f7', '--purple-bg': '#1c1130',
- '--surface': '#161b22', '--surface2': '#0d1117', '--surface3': '#010409',
- '--border-md': '#30363d', '--text': '#f0f6fc', '--text-2': '#8b949e', '--text-3': '#6e7681',
- '--ok': '#3fb950', '--warn': '#d29922', '--ai': '#a371f7', '--err': '#f85149'
- },
- light: {
- '--bg-primary': '#ffffff', '--bg-secondary': '#f6f8fa', '--bg-sidebar': '#f6f8fa',
- '--bg-hover': '#eaeef2', '--border': '#d0d7de', '--text-primary': '#1f2328',
- '--text-secondary': '#656d76', '--text-muted': '#8c959f', '--accent': '#0969da',
- '--accent-green': '#1a7f37', '--tag-bg': '#ddf4ff', '--tag-text': '#0969da',
- '--code-bg': '#f6f8fa', '--search-bg': '#ffffff', '--scrollbar': '#d0d7de',
- '--resize-handle': '#d0d7de', '--overlay-bg': 'rgba(0,0,0,0.25)',
- '--danger': '#cf222e', '--danger-bg': '#ffebe9', '--success': '#1a7f37', '--success-bg': '#dafbe1',
- '--accent-card': '#0969da', '--accent-bg': '#ddf4ff', '--accent-text': '#0969da',
- '--green': '#1a7f37', '--green-bg': '#dafbe1', '--purple': '#8250df', '--purple-bg': '#fbefff',
- '--surface': '#f6f8fa', '--surface2': '#ffffff', '--surface3': '#ebedf0',
- '--border-md': '#d0d7de', '--text': '#1f2328', '--text-2': '#656d76', '--text-3': '#8c959f',
- '--ok': '#1a7f37', '--warn': '#9a6700', '--ai': '#8250df', '--err': '#cf222e'
- }
- }
- },
-
- 'linear': {
- name: 'Linear', desc: 'Dégradés subtils',
- modes: {
- dark: {
- '--bg-primary': '#1a1a2e', '--bg-secondary': '#16162a', '--bg-sidebar': '#12121f',
- '--bg-hover': '#222240', '--border': '#2a2a45', '--text-primary': '#e1e1f5',
- '--text-secondary': '#8888aa', '--text-muted': '#555577', '--accent': '#5e6ad2',
- '--accent-green': '#2ecc71', '--tag-bg': '#222050', '--tag-text': '#8b95e0',
- '--code-bg': '#16162a', '--search-bg': '#1e1e38', '--scrollbar': '#2a2a45',
- '--resize-handle': '#2a2a45', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#e0556a', '--danger-bg': '#361520', '--success': '#2ecc71', '--success-bg': '#0d2818',
- '--accent-card': '#5e6ad2', '--accent-bg': '#222050', '--accent-text': '#8b95e0',
- '--green': '#2ecc71', '--green-bg': '#0d2818', '--purple': '#9b6bff', '--purple-bg': '#25183d',
- '--surface': '#16162a', '--surface2': '#1a1a2e', '--surface3': '#12121f',
- '--border-md': '#2a2a45', '--text': '#e1e1f5', '--text-2': '#8888aa', '--text-3': '#555577',
- '--ok': '#2ecc71', '--warn': '#f0b429', '--ai': '#9b6bff', '--err': '#e0556a'
- },
- light: {
- '--bg-primary': '#fafafa', '--bg-secondary': '#f0f0f5', '--bg-sidebar': '#e8e8f0',
- '--bg-hover': '#e0e0ea', '--border': '#d5d5e0', '--text-primary': '#1a1a2e',
- '--text-secondary': '#5c5c7a', '--text-muted': '#9090a8', '--accent': '#5e6ad2',
- '--accent-green': '#1a8b4a', '--tag-bg': '#e8e8ff', '--tag-text': '#5e6ad2',
- '--code-bg': '#f0f0f5', '--search-bg': '#fafafa', '--scrollbar': '#d5d5e0',
- '--resize-handle': '#d5d5e0', '--overlay-bg': 'rgba(0,0,0,0.2)',
- '--danger': '#c92a3e', '--danger-bg': '#ffeaea', '--success': '#1a8b4a', '--success-bg': '#e0ffe0',
- '--accent-card': '#5e6ad2', '--accent-bg': '#e8e8ff', '--accent-text': '#5e6ad2',
- '--green': '#1a8b4a', '--green-bg': '#e0ffe0', '--purple': '#7c5ce0', '--purple-bg': '#efe8ff',
- '--surface': '#f0f0f5', '--surface2': '#fafafa', '--surface3': '#e8e8f0',
- '--border-md': '#d5d5e0', '--text': '#1a1a2e', '--text-2': '#5c5c7a', '--text-3': '#9090a8',
- '--ok': '#1a8b4a', '--warn': '#9a6700', '--ai': '#7c5ce0', '--err': '#c92a3e'
- }
- }
- },
-
- 'spotify': {
- name: 'Spotify', desc: 'Noir pur & vert',
- modes: {
- dark: {
- '--bg-primary': '#000000', '--bg-secondary': '#121212', '--bg-sidebar': '#000000',
- '--bg-hover': '#1a1a1a', '--border': '#282828', '--text-primary': '#ffffff',
- '--text-secondary': '#b3b3b3', '--text-muted': '#727272', '--accent': '#1db954',
- '--accent-green': '#1db954', '--tag-bg': '#0d3317', '--tag-text': '#1db954',
- '--code-bg': '#121212', '--search-bg': '#242424', '--scrollbar': '#333333',
- '--resize-handle': '#282828', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#e22134', '--danger-bg': '#2a0a0e', '--success': '#1db954', '--success-bg': '#0d3317',
- '--accent-card': '#1db954', '--accent-bg': '#0d3317', '--accent-text': '#1db954',
- '--green': '#1db954', '--green-bg': '#0d3317', '--purple': '#af2896', '--purple-bg': '#2a0d33',
- '--surface': '#121212', '--surface2': '#000000', '--surface3': '#000000',
- '--border-md': '#282828', '--text': '#ffffff', '--text-2': '#b3b3b3', '--text-3': '#727272',
- '--ok': '#1db954', '--warn': '#f59b23', '--ai': '#af2896', '--err': '#e22134'
- },
- light: {
- '--bg-primary': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-sidebar': '#f0f0f0',
- '--bg-hover': '#e5e5e5', '--border': '#d5d5d5', '--text-primary': '#121212',
- '--text-secondary': '#575757', '--text-muted': '#8c8c8c', '--accent': '#1db954',
- '--accent-green': '#1db954', '--tag-bg': '#e0ffe0', '--tag-text': '#128a3c',
- '--code-bg': '#f5f5f5', '--search-bg': '#ffffff', '--scrollbar': '#d5d5d5',
- '--resize-handle': '#d5d5d5', '--overlay-bg': 'rgba(0,0,0,0.2)',
- '--danger': '#c92a2a', '--danger-bg': '#ffe3e3', '--success': '#1db954', '--success-bg': '#e0ffe0',
- '--accent-card': '#1db954', '--accent-bg': '#e0ffe0', '--accent-text': '#128a3c',
- '--green': '#1db954', '--green-bg': '#e0ffe0', '--purple': '#8a2be2', '--purple-bg': '#f3e0ff',
- '--surface': '#f5f5f5', '--surface2': '#ffffff', '--surface3': '#f0f0f0',
- '--border-md': '#d5d5d5', '--text': '#121212', '--text-2': '#575757', '--text-3': '#8c8c8c',
- '--ok': '#1db954', '--warn': '#9a6700', '--ai': '#8a2be2', '--err': '#c92a2a'
- }
- }
- },
-
- 'notion': {
- name: 'Notion', desc: 'Lecture propre',
- modes: {
- dark: {
- '--bg-primary': '#191919', '--bg-secondary': '#202020', '--bg-sidebar': '#191919',
- '--bg-hover': '#262626', '--border': '#333333', '--text-primary': '#e6e6e6',
- '--text-secondary': '#999999', '--text-muted': '#666666', '--accent': '#4b85e8',
- '--accent-green': '#0ca16a', '--tag-bg': '#1c2744', '--tag-text': '#6ba1f0',
- '--code-bg': '#202020', '--search-bg': '#262626', '--scrollbar': '#333333',
- '--resize-handle': '#333333', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#e03e3e', '--danger-bg': '#2a1010', '--success': '#0ca16a', '--success-bg': '#0d2818',
- '--accent-card': '#4b85e8', '--accent-bg': '#1c2744', '--accent-text': '#6ba1f0',
- '--green': '#0ca16a', '--green-bg': '#0d2818', '--purple': '#9065e0', '--purple-bg': '#24183d',
- '--surface': '#202020', '--surface2': '#191919', '--surface3': '#141414',
- '--border-md': '#333333', '--text': '#e6e6e6', '--text-2': '#999999', '--text-3': '#666666',
- '--ok': '#0ca16a', '--warn': '#dfab3e', '--ai': '#9065e0', '--err': '#e03e3e'
- },
- light: {
- '--bg-primary': '#ffffff', '--bg-secondary': '#f7f6f3', '--bg-sidebar': '#f7f6f3',
- '--bg-hover': '#edece9', '--border': '#e3e2e0', '--text-primary': '#37352f',
- '--text-secondary': '#787774', '--text-muted': '#abaaa7', '--accent': '#4b85e8',
- '--accent-green': '#0ca16a', '--tag-bg': '#e8f0fe', '--tag-text': '#4b85e8',
- '--code-bg': '#f7f6f3', '--search-bg': '#ffffff', '--scrollbar': '#e3e2e0',
- '--resize-handle': '#e3e2e0', '--overlay-bg': 'rgba(0,0,0,0.15)',
- '--danger': '#e03e3e', '--danger-bg': '#fdebec', '--success': '#0ca16a', '--success-bg': '#e6f7ec',
- '--accent-card': '#4b85e8', '--accent-bg': '#e8f0fe', '--accent-text': '#4b85e8',
- '--green': '#0ca16a', '--green-bg': '#e6f7ec', '--purple': '#9065e0', '--purple-bg': '#f3effc',
- '--surface': '#f7f6f3', '--surface2': '#ffffff', '--surface3': '#edece9',
- '--border-md': '#e3e2e0', '--text': '#37352f', '--text-2': '#787774', '--text-3': '#abaaa7',
- '--ok': '#0ca16a', '--warn': '#dfab3e', '--ai': '#9065e0', '--err': '#e03e3e'
- }
- }
- },
-
- 'discord': {
- name: 'Discord', desc: 'Standard communautaire',
- modes: {
- dark: {
- '--bg-primary': '#313338', '--bg-secondary': '#2b2d31', '--bg-sidebar': '#1e1f22',
- '--bg-hover': '#3c3e45', '--border': '#3f4147', '--text-primary': '#f2f3f5',
- '--text-secondary': '#b5bac1', '--text-muted': '#80848e', '--accent': '#5865f2',
- '--accent-green': '#23a559', '--tag-bg': '#28315c', '--tag-text': '#8891f2',
- '--code-bg': '#2b2d31', '--search-bg': '#383a40', '--scrollbar': '#1a1b1e',
- '--resize-handle': '#3f4147', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#da373c', '--danger-bg': '#3a1518', '--success': '#23a559', '--success-bg': '#122b1a',
- '--accent-card': '#5865f2', '--accent-bg': '#28315c', '--accent-text': '#8891f2',
- '--green': '#23a559', '--green-bg': '#122b1a', '--purple': '#9c5bf0', '--purple-bg': '#2c1a46',
- '--surface': '#2b2d31', '--surface2': '#313338', '--surface3': '#1e1f22',
- '--border-md': '#3f4147', '--text': '#f2f3f5', '--text-2': '#b5bac1', '--text-3': '#80848e',
- '--ok': '#23a559', '--warn': '#f0b232', '--ai': '#9c5bf0', '--err': '#da373c'
- },
- light: {
- '--bg-primary': '#ffffff', '--bg-secondary': '#f2f3f5', '--bg-sidebar': '#e3e5e8',
- '--bg-hover': '#e3e5e8', '--border': '#d4d7dc', '--text-primary': '#313338',
- '--text-secondary': '#5c5e66', '--text-muted': '#91949e', '--accent': '#5865f2',
- '--accent-green': '#248046', '--tag-bg': '#e8eafe', '--tag-text': '#4752c4',
- '--code-bg': '#f2f3f5', '--search-bg': '#ffffff', '--scrollbar': '#d4d7dc',
- '--resize-handle': '#d4d7dc', '--overlay-bg': 'rgba(0,0,0,0.2)',
- '--danger': '#d92028', '--danger-bg': '#fde8e8', '--success': '#248046', '--success-bg': '#e6f7e8',
- '--accent-card': '#5865f2', '--accent-bg': '#e8eafe', '--accent-text': '#4752c4',
- '--green': '#248046', '--green-bg': '#e6f7e8', '--purple': '#8b5cf6', '--purple-bg': '#f0ebff',
- '--surface': '#f2f3f5', '--surface2': '#ffffff', '--surface3': '#e3e5e8',
- '--border-md': '#d4d7dc', '--text': '#313338', '--text-2': '#5c5e66', '--text-3': '#91949e',
- '--ok': '#248046', '--warn': '#9a6700', '--ai': '#8b5cf6', '--err': '#d92028'
- }
- }
- },
-
- 'dracula': {
- name: 'Dracula', desc: 'Accents rose & violet',
- modes: {
- dark: {
- '--bg-primary': '#282a36', '--bg-secondary': '#21222c', '--bg-sidebar': '#1d1e26',
- '--bg-hover': '#343746', '--border': '#3b3d54', '--text-primary': '#f8f8f2',
- '--text-secondary': '#b2b2b2', '--text-muted': '#6272a4', '--accent': '#bd93f9',
- '--accent-green': '#50fa7b', '--tag-bg': '#2a2240', '--tag-text': '#bd93f9',
- '--code-bg': '#21222c', '--search-bg': '#343746', '--scrollbar': '#44475a',
- '--resize-handle': '#3b3d54', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#ff5555', '--danger-bg': '#3a1a1a', '--success': '#50fa7b', '--success-bg': '#0d2818',
- '--accent-card': '#bd93f9', '--accent-bg': '#2a2240', '--accent-text': '#bd93f9',
- '--green': '#50fa7b', '--green-bg': '#0d2818', '--purple': '#bd93f9', '--purple-bg': '#2a2240',
- '--surface': '#21222c', '--surface2': '#282a36', '--surface3': '#1d1e26',
- '--border-md': '#3b3d54', '--text': '#f8f8f2', '--text-2': '#b2b2b2', '--text-3': '#6272a4',
- '--ok': '#50fa7b', '--warn': '#f1fa8c', '--ai': '#bd93f9', '--err': '#ff5555'
- },
- light: {
- '--bg-primary': '#f8f8f2', '--bg-secondary': '#f0f0ea', '--bg-sidebar': '#e8e8e0',
- '--bg-hover': '#e0e0d8', '--border': '#d5d5cc', '--text-primary': '#282a36',
- '--text-secondary': '#4d5066', '--text-muted': '#8b8fa8', '--accent': '#8a5cf6',
- '--accent-green': '#3d8f54', '--tag-bg': '#ede0ff', '--tag-text': '#8a5cf6',
- '--code-bg': '#f0f0ea', '--search-bg': '#f8f8f2', '--scrollbar': '#d5d5cc',
- '--resize-handle': '#d5d5cc', '--overlay-bg': 'rgba(0,0,0,0.15)',
- '--danger': '#d7373f', '--danger-bg': '#fde8e8', '--success': '#3d8f54', '--success-bg': '#e6f7e8',
- '--accent-card': '#8a5cf6', '--accent-bg': '#ede0ff', '--accent-text': '#8a5cf6',
- '--green': '#3d8f54', '--green-bg': '#e6f7e8', '--purple': '#8a5cf6', '--purple-bg': '#ede0ff',
- '--surface': '#f0f0ea', '--surface2': '#f8f8f2', '--surface3': '#e8e8e0',
- '--border-md': '#d5d5cc', '--text': '#282a36', '--text-2': '#4d5066', '--text-3': '#8b8fa8',
- '--ok': '#3d8f54', '--warn': '#8b7500', '--ai': '#8a5cf6', '--err': '#d7373f'
- }
- }
- },
-
- 'one-dark': {
- name: 'One Dark', desc: 'Inspire d\'Atom',
- modes: {
- dark: {
- '--bg-primary': '#282c34', '--bg-secondary': '#21252b', '--bg-sidebar': '#1c1f26',
- '--bg-hover': '#333842', '--border': '#3e4452', '--text-primary': '#abb2bf',
- '--text-secondary': '#828997', '--text-muted': '#5c6370', '--accent': '#61afef',
- '--accent-green': '#98c379', '--tag-bg': '#1e3350', '--tag-text': '#61afef',
- '--code-bg': '#21252b', '--search-bg': '#333842', '--scrollbar': '#3e4452',
- '--resize-handle': '#3e4452', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#e06c75', '--danger-bg': '#351b1e', '--success': '#98c379', '--success-bg': '#1a2e1a',
- '--accent-card': '#61afef', '--accent-bg': '#1e3350', '--accent-text': '#61afef',
- '--green': '#98c379', '--green-bg': '#1a2e1a', '--purple': '#c678dd', '--purple-bg': '#2c1a3a',
- '--surface': '#21252b', '--surface2': '#282c34', '--surface3': '#1c1f26',
- '--border-md': '#3e4452', '--text': '#abb2bf', '--text-2': '#828997', '--text-3': '#5c6370',
- '--ok': '#98c379', '--warn': '#e5c07b', '--ai': '#c678dd', '--err': '#e06c75'
- },
- light: {
- '--bg-primary': '#fafafa', '--bg-secondary': '#f0f0f0', '--bg-sidebar': '#e8e8e8',
- '--bg-hover': '#e0e0e0', '--border': '#d4d4d4', '--text-primary': '#383a42',
- '--text-secondary': '#696c77', '--text-muted': '#9ca0a4', '--accent': '#4078f2',
- '--accent-green': '#50a14f', '--tag-bg': '#e0e8ff', '--tag-text': '#4078f2',
- '--code-bg': '#f0f0f0', '--search-bg': '#fafafa', '--scrollbar': '#d4d4d4',
- '--resize-handle': '#d4d4d4', '--overlay-bg': 'rgba(0,0,0,0.15)',
- '--danger': '#e45649', '--danger-bg': '#fde8e8', '--success': '#50a14f', '--success-bg': '#e6f7e6',
- '--accent-card': '#4078f2', '--accent-bg': '#e0e8ff', '--accent-text': '#4078f2',
- '--green': '#50a14f', '--green-bg': '#e6f7e6', '--purple': '#a626a4', '--purple-bg': '#f8e0f8',
- '--surface': '#f0f0f0', '--surface2': '#fafafa', '--surface3': '#e8e8e8',
- '--border-md': '#d4d4d4', '--text': '#383a42', '--text-2': '#696c77', '--text-3': '#9ca0a4',
- '--ok': '#50a14f', '--warn': '#986801', '--ai': '#a626a4', '--err': '#e45649'
- }
- }
- },
-
- 'tokyo-night': {
- name: 'Tokyo Night', desc: 'Bleu neon',
- modes: {
- dark: {
- '--bg-primary': '#1a1b26', '--bg-secondary': '#16161e', '--bg-sidebar': '#13131a',
- '--bg-hover': '#24283b', '--border': '#2f3545', '--text-primary': '#c0caf5',
- '--text-secondary': '#a9b1d6', '--text-muted': '#565f89', '--accent': '#7aa2f7',
- '--accent-green': '#9ece6a', '--tag-bg': '#1f2845', '--tag-text': '#7aa2f7',
- '--code-bg': '#16161e', '--search-bg': '#24283b', '--scrollbar': '#2f3545',
- '--resize-handle': '#2f3545', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#f7768e', '--danger-bg': '#361a22', '--success': '#9ece6a', '--success-bg': '#1a2e1a',
- '--accent-card': '#7aa2f7', '--accent-bg': '#1f2845', '--accent-text': '#7aa2f7',
- '--green': '#9ece6a', '--green-bg': '#1a2e1a', '--purple': '#bb9af7', '--purple-bg': '#241a3a',
- '--surface': '#16161e', '--surface2': '#1a1b26', '--surface3': '#13131a',
- '--border-md': '#2f3545', '--text': '#c0caf5', '--text-2': '#a9b1d6', '--text-3': '#565f89',
- '--ok': '#9ece6a', '--warn': '#e0af68', '--ai': '#bb9af7', '--err': '#f7768e'
- },
- light: {
- '--bg-primary': '#d5d6db', '--bg-secondary': '#c8c9d0', '--bg-sidebar': '#bfc0c8',
- '--bg-hover': '#bbbbc4', '--border': '#a9aab3', '--text-primary': '#343b58',
- '--text-secondary': '#565a6e', '--text-muted': '#848cb0', '--accent': '#34548a',
- '--accent-green': '#485e30', '--tag-bg': '#dfe5f5', '--tag-text': '#34548a',
- '--code-bg': '#c8c9d0', '--search-bg': '#d5d6db', '--scrollbar': '#a9aab3',
- '--resize-handle': '#a9aab3', '--overlay-bg': 'rgba(0,0,0,0.15)',
- '--danger': '#8c4351', '--danger-bg': '#f8e0e0', '--success': '#485e30', '--success-bg': '#e0f0d8',
- '--accent-card': '#34548a', '--accent-bg': '#dfe5f5', '--accent-text': '#34548a',
- '--green': '#485e30', '--green-bg': '#e0f0d8', '--purple': '#5a4a9a', '--purple-bg': '#e8e0f8',
- '--surface': '#c8c9d0', '--surface2': '#d5d6db', '--surface3': '#bfc0c8',
- '--border-md': '#a9aab3', '--text': '#343b58', '--text-2': '#565a6e', '--text-3': '#848cb0',
- '--ok': '#485e30', '--warn': '#8f5e15', '--ai': '#5a4a9a', '--err': '#8c4351'
- }
- }
- },
-
- 'nord': {
- name: 'Nord', desc: 'Style arctique',
- modes: {
- dark: {
- '--bg-primary': '#2e3440', '--bg-secondary': '#3b4252', '--bg-sidebar': '#242933',
- '--bg-hover': '#434c5e', '--border': '#4c566a', '--text-primary': '#eceff4',
- '--text-secondary': '#d8dee9', '--text-muted': '#7b88a1', '--accent': '#88c0d0',
- '--accent-green': '#a3be8c', '--tag-bg': '#2a3a48', '--tag-text': '#88c0d0',
- '--code-bg': '#3b4252', '--search-bg': '#434c5e', '--scrollbar': '#4c566a',
- '--resize-handle': '#4c566a', '--overlay-bg': 'rgba(0,0,0,0.6)',
- '--danger': '#bf616a', '--danger-bg': '#3a2025', '--success': '#a3be8c', '--success-bg': '#1a2e1a',
- '--accent-card': '#88c0d0', '--accent-bg': '#2a3a48', '--accent-text': '#88c0d0',
- '--green': '#a3be8c', '--green-bg': '#1a2e1a', '--purple': '#b48ead', '--purple-bg': '#2a2030',
- '--surface': '#3b4252', '--surface2': '#2e3440', '--surface3': '#242933',
- '--border-md': '#4c566a', '--text': '#eceff4', '--text-2': '#d8dee9', '--text-3': '#7b88a1',
- '--ok': '#a3be8c', '--warn': '#ebcb8b', '--ai': '#b48ead', '--err': '#bf616a'
- },
- light: {
- '--bg-primary': '#eceff4', '--bg-secondary': '#e5e9f0', '--bg-sidebar': '#d8dee9',
- '--bg-hover': '#d8dee9', '--border': '#c8cfdb', '--text-primary': '#2e3440',
- '--text-secondary': '#4c566a', '--text-muted': '#7b88a1', '--accent': '#5e81ac',
- '--accent-green': '#719350', '--tag-bg': '#dfe8f0', '--tag-text': '#5e81ac',
- '--code-bg': '#e5e9f0', '--search-bg': '#eceff4', '--scrollbar': '#c8cfdb',
- '--resize-handle': '#c8cfdb', '--overlay-bg': 'rgba(0,0,0,0.12)',
- '--danger': '#bf616a', '--danger-bg': '#f8e0e0', '--success': '#719350', '--success-bg': '#e0f0d8',
- '--accent-card': '#5e81ac', '--accent-bg': '#dfe8f0', '--accent-text': '#5e81ac',
- '--green': '#719350', '--green-bg': '#e0f0d8', '--purple': '#7b5e95', '--purple-bg': '#e8dff0',
- '--surface': '#e5e9f0', '--surface2': '#eceff4', '--surface3': '#d8dee9',
- '--border-md': '#c8cfdb', '--text': '#2e3440', '--text-2': '#4c566a', '--text-3': '#7b88a1',
- '--ok': '#719350', '--warn': '#8f6b20', '--ai': '#7b5e95', '--err': '#bf616a'
- }
- }
- },
-
- 'night-owl': {
- name: 'Night Owl', desc: 'Optimise pour la nuit',
- modes: {
- dark: {
- '--bg-primary': '#011627', '--bg-secondary': '#01111d', '--bg-sidebar': '#010d18',
- '--bg-hover': '#0b2942', '--border': '#1d3b53', '--text-primary': '#d6deeb',
- '--text-secondary': '#7e97b0', '--text-muted': '#4b6479', '--accent': '#82aaff',
- '--accent-green': '#c3e88d', '--tag-bg': '#152d45', '--tag-text': '#82aaff',
- '--code-bg': '#01111d', '--search-bg': '#0b2942', '--scrollbar': '#1d3b53',
- '--resize-handle': '#1d3b53', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#ef5350', '--danger-bg': '#2a1018', '--success': '#c3e88d', '--success-bg': '#0d2818',
- '--accent-card': '#82aaff', '--accent-bg': '#152d45', '--accent-text': '#82aaff',
- '--green': '#c3e88d', '--green-bg': '#0d2818', '--purple': '#c792ea', '--purple-bg': '#201838',
- '--surface': '#01111d', '--surface2': '#011627', '--surface3': '#010d18',
- '--border-md': '#1d3b53', '--text': '#d6deeb', '--text-2': '#7e97b0', '--text-3': '#4b6479',
- '--ok': '#c3e88d', '--warn': '#ffcb8b', '--ai': '#c792ea', '--err': '#ef5350'
- },
- light: {
- '--bg-primary': '#fbfbfb', '--bg-secondary': '#f0f0f0', '--bg-sidebar': '#e8e8e8',
- '--bg-hover': '#e0e0e0', '--border': '#d4d4d4', '--text-primary': '#403f53',
- '--text-secondary': '#5f6c7a', '--text-muted': '#8e99a8', '--accent': '#4876d6',
- '--accent-green': '#2d8a4e', '--tag-bg': '#e0e8ff', '--tag-text': '#4876d6',
- '--code-bg': '#f0f0f0', '--search-bg': '#fbfbfb', '--scrollbar': '#d4d4d4',
- '--resize-handle': '#d4d4d4', '--overlay-bg': 'rgba(0,0,0,0.12)',
- '--danger': '#c92a2a', '--danger-bg': '#ffe3e3', '--success': '#2d8a4e', '--success-bg': '#e0ffe0',
- '--accent-card': '#4876d6', '--accent-bg': '#e0e8ff', '--accent-text': '#4876d6',
- '--green': '#2d8a4e', '--green-bg': '#e0ffe0', '--purple': '#7c3aed', '--purple-bg': '#ede0ff',
- '--surface': '#f0f0f0', '--surface2': '#fbfbfb', '--surface3': '#e8e8e8',
- '--border-md': '#d4d4d4', '--text': '#403f53', '--text-2': '#5f6c7a', '--text-3': '#8e99a8',
- '--ok': '#2d8a4e', '--warn': '#8f6b20', '--ai': '#7c3aed', '--err': '#c92a2a'
- }
- }
- },
-
- 'anthracite': {
- name: 'Anthracite', desc: 'Material dark',
- modes: {
- dark: {
- '--bg-primary': '#1e1e1e', '--bg-secondary': '#252526', '--bg-sidebar': '#1a1a1a',
- '--bg-hover': '#2d2d2d', '--border': '#3e3e3e', '--text-primary': '#d4d4d4',
- '--text-secondary': '#9d9d9d', '--text-muted': '#6e6e6e', '--accent': '#9c7aef',
- '--accent-green': '#6a9955', '--tag-bg': '#2a2250', '--tag-text': '#9c7aef',
- '--code-bg': '#252526', '--search-bg': '#2d2d2d', '--scrollbar': '#3e3e3e',
- '--resize-handle': '#3e3e3e', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#f44747', '--danger-bg': '#3a1a1a', '--success': '#6a9955', '--success-bg': '#1a2e1a',
- '--accent-card': '#9c7aef', '--accent-bg': '#2a2250', '--accent-text': '#9c7aef',
- '--green': '#6a9955', '--green-bg': '#1a2e1a', '--purple': '#9c7aef', '--purple-bg': '#2a2250',
- '--surface': '#252526', '--surface2': '#1e1e1e', '--surface3': '#1a1a1a',
- '--border-md': '#3e3e3e', '--text': '#d4d4d4', '--text-2': '#9d9d9d', '--text-3': '#6e6e6e',
- '--ok': '#6a9955', '--warn': '#d7ba7d', '--ai': '#9c7aef', '--err': '#f44747'
- },
- light: {
- '--bg-primary': '#fafafa', '--bg-secondary': '#f5f5f5', '--bg-sidebar': '#eeeeee',
- '--bg-hover': '#e5e5e5', '--border': '#d5d5d5', '--text-primary': '#212121',
- '--text-secondary': '#616161', '--text-muted': '#9e9e9e', '--accent': '#7c3aed',
- '--accent-green': '#388e3c', '--tag-bg': '#ede0ff', '--tag-text': '#7c3aed',
- '--code-bg': '#f5f5f5', '--search-bg': '#fafafa', '--scrollbar': '#d5d5d5',
- '--resize-handle': '#d5d5d5', '--overlay-bg': 'rgba(0,0,0,0.12)',
- '--danger': '#d32f2f', '--danger-bg': '#fde8e8', '--success': '#388e3c', '--success-bg': '#e8f5e9',
- '--accent-card': '#7c3aed', '--accent-bg': '#ede0ff', '--accent-text': '#7c3aed',
- '--green': '#388e3c', '--green-bg': '#e8f5e9', '--purple': '#7c3aed', '--purple-bg': '#ede0ff',
- '--surface': '#f5f5f5', '--surface2': '#fafafa', '--surface3': '#eeeeee',
- '--border-md': '#d5d5d5', '--text': '#212121', '--text-2': '#616161', '--text-3': '#9e9e9e',
- '--ok': '#388e3c', '--warn': '#8f6b20', '--ai': '#7c3aed', '--err': '#d32f2f'
- }
- }
- },
-
- 'cyberpunk': {
- name: 'Cyberpunk', desc: 'Neon vibrant',
- modes: {
- dark: {
- '--bg-primary': '#0a0a0f', '--bg-secondary': '#111122', '--bg-sidebar': '#08080d',
- '--bg-hover': '#1a1a33', '--border': '#2a2a55', '--text-primary': '#e0e0ff',
- '--text-secondary': '#8888cc', '--text-muted': '#555599', '--accent': '#00ffff',
- '--accent-green': '#00ff88', '--tag-bg': '#0a2a3a', '--tag-text': '#00ffff',
- '--code-bg': '#111122', '--search-bg': '#1a1a33', '--scrollbar': '#2a2a55',
- '--resize-handle': '#2a2a55', '--overlay-bg': 'rgba(0,0,0,0.8)',
- '--danger': '#ff3366', '--danger-bg': '#331122', '--success': '#00ff88', '--success-bg': '#003322',
- '--accent-card': '#00ffff', '--accent-bg': '#0a2a3a', '--accent-text': '#00ffff',
- '--green': '#00ff88', '--green-bg': '#003322', '--purple': '#ff00ff', '--purple-bg': '#220033',
- '--surface': '#111122', '--surface2': '#0a0a0f', '--surface3': '#08080d',
- '--border-md': '#2a2a55', '--text': '#e0e0ff', '--text-2': '#8888cc', '--text-3': '#555599',
- '--ok': '#00ff88', '--warn': '#ffaa00', '--ai': '#ff00ff', '--err': '#ff3366'
- },
- light: {
- '--bg-primary': '#f0f0ff', '--bg-secondary': '#e8e8ff', '--bg-sidebar': '#e0e0f8',
- '--bg-hover': '#d8d8f0', '--border': '#c0c0e0', '--text-primary': '#1a1a33',
- '--text-secondary': '#4a4a6a', '--text-muted': '#8080a0', '--accent': '#0077cc',
- '--accent-green': '#008844', '--tag-bg': '#ddeeff', '--tag-text': '#0077cc',
- '--code-bg': '#e8e8ff', '--search-bg': '#f0f0ff', '--scrollbar': '#c0c0e0',
- '--resize-handle': '#c0c0e0', '--overlay-bg': 'rgba(0,0,0,0.1)',
- '--danger': '#cc2244', '--danger-bg': '#ffe0e8', '--success': '#008844', '--success-bg': '#d8ffe0',
- '--accent-card': '#0077cc', '--accent-bg': '#ddeeff', '--accent-text': '#0077cc',
- '--green': '#008844', '--green-bg': '#d8ffe0', '--purple': '#8800cc', '--purple-bg': '#f0d8ff',
- '--surface': '#e8e8ff', '--surface2': '#f0f0ff', '--surface3': '#e0e0f8',
- '--border-md': '#c0c0e0', '--text': '#1a1a33', '--text-2': '#4a4a6a', '--text-3': '#8080a0',
- '--ok': '#008844', '--warn': '#996600', '--ai': '#8800cc', '--err': '#cc2244'
- }
- }
- },
-
- 'navy-elegance': {
- name: 'Navy Elegance', desc: 'Bleu profond & or',
- modes: {
- dark: {
- '--bg-primary': '#0a1628', '--bg-secondary': '#0d1f3c', '--bg-sidebar': '#081220',
- '--bg-hover': '#162d50', '--border': '#1e3a5f', '--text-primary': '#d4dde8',
- '--text-secondary': '#8899b4', '--text-muted': '#556688', '--accent': '#c9a84c',
- '--accent-green': '#4caf7c', '--tag-bg': '#1e2840', '--tag-text': '#c9a84c',
- '--code-bg': '#0d1f3c', '--search-bg': '#162d50', '--scrollbar': '#1e3a5f',
- '--resize-handle': '#1e3a5f', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#d94a5a', '--danger-bg': '#351a22', '--success': '#4caf7c', '--success-bg': '#12281e',
- '--accent-card': '#c9a84c', '--accent-bg': '#282218', '--accent-text': '#c9a84c',
- '--green': '#4caf7c', '--green-bg': '#12281e', '--purple': '#8b6fc0', '--purple-bg': '#201a3a',
- '--surface': '#0d1f3c', '--surface2': '#0a1628', '--surface3': '#081220',
- '--border-md': '#1e3a5f', '--text': '#d4dde8', '--text-2': '#8899b4', '--text-3': '#556688',
- '--ok': '#4caf7c', '--warn': '#c9a84c', '--ai': '#8b6fc0', '--err': '#d94a5a'
- },
- light: {
- '--bg-primary': '#f5f0e8', '--bg-secondary': '#ede6d8', '--bg-sidebar': '#e8dfd0',
- '--bg-hover': '#e0d8c8', '--border': '#c8bda8', '--text-primary': '#2a3a50',
- '--text-secondary': '#5a6a80', '--text-muted': '#8a9aa8', '--accent': '#8b6914',
- '--accent-green': '#3d7a4c', '--tag-bg': '#f0e8d0', '--tag-text': '#8b6914',
- '--code-bg': '#ede6d8', '--search-bg': '#f5f0e8', '--scrollbar': '#c8bda8',
- '--resize-handle': '#c8bda8', '--overlay-bg': 'rgba(0,0,0,0.1)',
- '--danger': '#b8383a', '--danger-bg': '#f8e0e0', '--success': '#3d7a4c', '--success-bg': '#e0f0e0',
- '--accent-card': '#8b6914', '--accent-bg': '#f0e8d0', '--accent-text': '#8b6914',
- '--green': '#3d7a4c', '--green-bg': '#e0f0e0', '--purple': '#6a4c8a', '--purple-bg': '#e8dff0',
- '--surface': '#ede6d8', '--surface2': '#f5f0e8', '--surface3': '#e8dfd0',
- '--border-md': '#c8bda8', '--text': '#2a3a50', '--text-2': '#5a6a80', '--text-3': '#8a9aa8',
- '--ok': '#3d7a4c', '--warn': '#8b6914', '--ai': '#6a4c8a', '--err': '#b8383a'
- }
- }
- },
-
- 'tons-terreux': {
- name: 'Tons Terreux', desc: 'Sensation organique',
- modes: {
- dark: {
- '--bg-primary': '#1c1a16', '--bg-secondary': '#252218', '--bg-sidebar': '#161410',
- '--bg-hover': '#302b20', '--border': '#3d3628', '--text-primary': '#e8e0d0',
- '--text-secondary': '#a89880', '--text-muted': '#6b5d48', '--accent': '#c8a050',
- '--accent-green': '#6b9b50', '--tag-bg': '#2a2418', '--tag-text': '#c8a050',
- '--code-bg': '#252218', '--search-bg': '#302b20', '--scrollbar': '#3d3628',
- '--resize-handle': '#3d3628', '--overlay-bg': 'rgba(0,0,0,0.7)',
- '--danger': '#c86050', '--danger-bg': '#352018', '--success': '#6b9b50', '--success-bg': '#1a2a14',
- '--accent-card': '#c8a050', '--accent-bg': '#2a2418', '--accent-text': '#c8a050',
- '--green': '#6b9b50', '--green-bg': '#1a2a14', '--purple': '#9070b0', '--purple-bg': '#221a30',
- '--surface': '#252218', '--surface2': '#1c1a16', '--surface3': '#161410',
- '--border-md': '#3d3628', '--text': '#e8e0d0', '--text-2': '#a89880', '--text-3': '#6b5d48',
- '--ok': '#6b9b50', '--warn': '#c8a050', '--ai': '#9070b0', '--err': '#c86050'
- },
- light: {
- '--bg-primary': '#f8f4ec', '--bg-secondary': '#f0e8d8', '--bg-sidebar': '#e8e0d0',
- '--bg-hover': '#e0d8c8', '--border': '#c8bda0', '--text-primary': '#3d3028',
- '--text-secondary': '#6b5d48', '--text-muted': '#9a8a70', '--accent': '#8b6914',
- '--accent-green': '#5a8440', '--tag-bg': '#f0e0c8', '--tag-text': '#8b6914',
- '--code-bg': '#f0e8d8', '--search-bg': '#f8f4ec', '--scrollbar': '#c8bda0',
- '--resize-handle': '#c8bda0', '--overlay-bg': 'rgba(0,0,0,0.1)',
- '--danger': '#a84040', '--danger-bg': '#f8e0e0', '--success': '#5a8440', '--success-bg': '#e0f0d8',
- '--accent-card': '#8b6914', '--accent-bg': '#f0e0c8', '--accent-text': '#8b6914',
- '--green': '#5a8440', '--green-bg': '#e0f0d8', '--purple': '#7058a0', '--purple-bg': '#e8dff8',
- '--surface': '#f0e8d8', '--surface2': '#f8f4ec', '--surface3': '#e8e0d0',
- '--border-md': '#c8bda0', '--text': '#3d3028', '--text-2': '#6b5d48', '--text-3': '#9a8a70',
- '--ok': '#5a8440', '--warn': '#8b6914', '--ai': '#7058a0', '--err': '#a84040'
- }
- }
- }
-};
-
-// ── Theme Engine ─────────────────────────────────────────────────────
-var _currentTheme = 'defaut-obsigate';
-var _currentMode = 'dark';
-
-function applyTheme(themeKey, mode) {
- var theme = THEMES[themeKey];
- if (!theme) return;
- mode = mode || _currentMode || 'dark';
- var vars = theme.modes[mode];
- if (!vars) return;
-
- var root = document.documentElement;
- Object.keys(vars).forEach(function(key) {
- root.style.setProperty(key, vars[key]);
- });
-
- _currentTheme = themeKey;
- _currentMode = mode;
- try { localStorage.setItem('obsigate-theme', themeKey); } catch(e) {}
- try { localStorage.setItem('obsigate-theme-mode', mode); } catch(e) {}
-}
-
-function toggleThemeMode() {
- var newMode = _currentMode === 'dark' ? 'light' : 'dark';
- applyTheme(_currentTheme, newMode);
- return newMode;
-}
-
-function getCurrentTheme() { return _currentTheme; }
-function getCurrentMode() { return _currentMode; }
-
-function initThemes() {
- var saved = null;
- var savedMode = null;
- try { saved = localStorage.getItem('obsigate-theme'); } catch(e) {}
- try { savedMode = localStorage.getItem('obsigate-theme-mode'); } catch(e) {}
- if (saved && THEMES[saved]) _currentTheme = saved;
- if (savedMode) _currentMode = savedMode;
- applyTheme(_currentTheme, _currentMode);
-}
-
-export { THEMES, applyTheme, toggleThemeMode, getCurrentTheme, getCurrentMode, initThemes };
diff --git a/desktop/frontend-static/js/ui.js b/desktop/frontend-static/js/ui.js
deleted file mode 100644
index 0c4ec8e..0000000
--- a/desktop/frontend-static/js/ui.js
+++ /dev/null
@@ -1,2351 +0,0 @@
-/* ObsiGate — UI module */
-import { api, AuthManager, initLoginForm } from './auth.js';
-import { state } from './state.js';
-import { openFile, showWelcome, renderFile, el } from './viewer.js';
-import { safeCreateIcons, getFileIcon, escapeHtml } from './utils.js';
-import { syncActiveFileTreeItem, refreshSidebarTreePreservingState } from './sidebar.js';
-import { GraphViewManager } from './graph.js';
-import { DashboardConflictsWidget } from './dashboard.js';
-import { t, tr } from './i18n.js';
-import { toggleThemeMode } from './themes.js';
-// ---------------------------------------------------------------------------
-// Right Sidebar Manager
-// ---------------------------------------------------------------------------
-
-export 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) {
- state.rightSidebarVisible = savedVisible === "true";
- }
-
- if (savedWidth) {
- state.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 (state.rightSidebarVisible) {
- sidebar.classList.remove("hidden");
- sidebar.style.width = `${state.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() {
- state.rightSidebarVisible = !state.rightSidebarVisible;
- localStorage.setItem("obsigate-right-sidebar-visible", state.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`;
- state.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", state.rightSidebarWidth);
- };
-
- handle.addEventListener("mousedown", onMouseDown);
- document.addEventListener("mousemove", onMouseMove);
- document.addEventListener("mouseup", onMouseUp);
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Theme
-// ---------------------------------------------------------------------------
-export function initTheme() {
- const saved = localStorage.getItem("obsigate-theme") || "dark";
- applyTheme(saved);
-}
-
-export function applyTheme(theme) {
- document.documentElement.setAttribute("data-theme", theme);
- localStorage.setItem("obsigate-theme", theme);
-
- // Update theme button icon and label
- const themeBtn = document.getElementById("theme-toggle");
- const themeLabel = document.getElementById("theme-label");
- if (themeBtn && themeLabel) {
- const icon = themeBtn.querySelector("i");
- if (icon) {
- icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun");
- }
- themeLabel.textContent = theme === "dark" ? t('theme.dark') : t('theme.light');
- 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";
- }
-
- // Update Mermaid theme for newly rendered diagrams
- import('./mermaid-viewer.js').then(function(m) {
- m.updateMermaidTheme(theme === 'dark');
- }).catch(function() {});
-}
-
-export function toggleTheme() {
- const newMode = toggleThemeMode();
- // Update the theme toggle label
- const themeLabel = document.getElementById("theme-label");
- if (themeLabel) {
- themeLabel.textContent = newMode === "dark" ? t('theme.dark') : t('theme.light');
- }
- // Update the theme toggle icon
- const themeBtn = document.getElementById("theme-toggle");
- if (themeBtn) {
- const icon = themeBtn.querySelector("i");
- if (icon) {
- icon.setAttribute("data-lucide", newMode === "dark" ? "moon" : "sun");
- }
- }
- safeCreateIcons();
-}
-
-export function initHeaderMenu() {
- const menuBtn = document.getElementById("header-menu-btn");
- const menuDropdown = document.getElementById("header-menu-dropdown");
-
- if (!menuBtn || !menuDropdown) return;
-
- menuBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- menuBtn.classList.toggle("active");
- menuDropdown.classList.toggle("active");
- });
-
- // Close menu when clicking outside
- document.addEventListener("click", (e) => {
- if (!menuDropdown.contains(e.target) && e.target !== menuBtn) {
- menuBtn.classList.remove("active");
- menuDropdown.classList.remove("active");
- }
- });
-
- // Prevent menu from closing when clicking inside
- menuDropdown.addEventListener("click", (e) => {
- e.stopPropagation();
- });
-}
-
-export function closeHeaderMenu() {
- const menuBtn = document.getElementById("header-menu-btn");
- const menuDropdown = document.getElementById("header-menu-dropdown");
- if (!menuBtn || !menuDropdown) return;
- menuBtn.classList.remove("active");
- menuDropdown.classList.remove("active");
-}
-
-
-// ---------------------------------------------------------------------------
-// Custom Dropdowns
-// ---------------------------------------------------------------------------
-export function initCustomDropdowns() {
- document.querySelectorAll(".custom-dropdown").forEach((dropdown) => {
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- const options = dropdown.querySelectorAll(".custom-dropdown-option");
- const hiddenInput = dropdown.querySelector('input[type="hidden"]');
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
- const menu = dropdown.querySelector(".custom-dropdown-menu");
-
- if (!trigger) return;
-
- // Toggle dropdown
- trigger.addEventListener("click", (e) => {
- e.stopPropagation();
- const isOpen = dropdown.classList.contains("open");
-
- // Close all other dropdowns
- document.querySelectorAll(".custom-dropdown.open").forEach((d) => {
- if (d !== dropdown) d.classList.remove("open");
- });
-
- dropdown.classList.toggle("open", !isOpen);
- trigger.setAttribute("aria-expanded", !isOpen);
-
- // Position fixed menu for sidebar dropdowns
- if (!isOpen && dropdown.classList.contains("sidebar-dropdown") && menu) {
- const rect = trigger.getBoundingClientRect();
- menu.style.top = `${rect.bottom + 4}px`;
- menu.style.left = `${rect.left}px`;
- menu.style.width = `${rect.width}px`;
- }
- });
-
- // Handle option selection
- options.forEach((option) => {
- option.addEventListener("click", (e) => {
- e.stopPropagation();
- const value = option.getAttribute("data-value");
- const text = option.textContent;
-
- // Update hidden input
- if (hiddenInput) {
- hiddenInput.value = value;
- // Trigger change event
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
- }
-
- // Update selected text
- if (selectedText) {
- selectedText.textContent = text;
- }
-
- // Update visual selection
- options.forEach((opt) => opt.classList.remove("selected"));
- option.classList.add("selected");
-
- // Close dropdown
- dropdown.classList.remove("open");
- trigger.setAttribute("aria-expanded", "false");
- });
- });
- });
-
- // Close dropdowns when clicking outside
- document.addEventListener("click", () => {
- document.querySelectorAll(".custom-dropdown.open").forEach((dropdown) => {
- dropdown.classList.remove("open");
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- if (trigger) trigger.setAttribute("aria-expanded", "false");
- });
- });
-}
-
-// Helper to populate custom dropdown options
-export function populateCustomDropdown(dropdownId, optionsList, defaultValue) {
- const dropdown = document.getElementById(dropdownId);
- if (!dropdown) return;
-
- const optionsContainer = dropdown.querySelector(".custom-dropdown-menu");
- const hiddenInput = dropdown.querySelector('input[type="hidden"]');
- const selectedText = dropdown.querySelector(".custom-dropdown-selected");
-
- if (!optionsContainer) return;
-
- // Clear existing options (keep the first one if it's the default)
- optionsContainer.innerHTML = "";
-
- // Add new options
- optionsList.forEach((opt) => {
- const li = document.createElement("li");
- li.className = "custom-dropdown-option";
- li.setAttribute("role", "option");
- li.setAttribute("data-value", opt.value);
- li.textContent = opt.text;
- if (opt.value === defaultValue) {
- li.classList.add("selected");
- if (selectedText) selectedText.textContent = opt.text;
- if (hiddenInput) hiddenInput.value = opt.value;
- }
- optionsContainer.appendChild(li);
- });
-
- // Re-initialize click handlers
- optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((option) => {
- option.addEventListener("click", (e) => {
- e.stopPropagation();
- const value = option.getAttribute("data-value");
- const text = option.textContent;
-
- if (hiddenInput) {
- hiddenInput.value = value;
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
- }
-
- if (selectedText) {
- selectedText.textContent = text;
- }
-
- optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((opt) => opt.classList.remove("selected"));
- option.classList.add("selected");
-
- dropdown.classList.remove("open");
- const trigger = dropdown.querySelector(".custom-dropdown-trigger");
- if (trigger) trigger.setAttribute("aria-expanded", "false");
- });
- });
-}
-
-
-// ---------------------------------------------------------------------------
-// Toast notifications
-// ---------------------------------------------------------------------------
-
-/** Display a brief toast message at the bottom of the viewport. */
-export function showToast(message, type) {
- // Auto-translate French messages via reverse lookup
- message = tr(message);
- type = type || "info";
- let container = document.getElementById("toast-container");
- if (!container) {
- container = document.createElement("div");
- container.id = "toast-container";
- container.className = "toast-container";
- container.setAttribute("aria-live", "polite");
- document.body.appendChild(container);
- }
- var toast = document.createElement("div");
- toast.className = "toast toast-" + type;
- toast.textContent = message;
- container.appendChild(toast);
- // Trigger entrance animation
- requestAnimationFrame(function () {
- toast.classList.add("show");
- });
- setTimeout(function () {
- toast.classList.remove("show");
- toast.addEventListener("transitionend", function () {
- toast.remove();
- });
- }, 3500);
-}
-
-
-// ---------------------------------------------------------------------------
-// Sidebar toggle (desktop)
-// ---------------------------------------------------------------------------
-export function initSidebarToggle() {
- const toggleBtn = document.getElementById("sidebar-toggle-btn");
- const sidebar = document.getElementById("sidebar");
- const resizeHandle = document.getElementById("sidebar-resize-handle");
-
- if (!toggleBtn || !sidebar || !resizeHandle) return;
-
- // Restore saved state
- const savedState = localStorage.getItem("obsigate-sidebar-hidden");
- if (savedState === "true") {
- sidebar.classList.add("hidden");
- resizeHandle.classList.add("hidden");
- toggleBtn.classList.add("active");
- }
-
- toggleBtn.addEventListener("click", () => {
- const isHidden = sidebar.classList.toggle("hidden");
- resizeHandle.classList.toggle("hidden", isHidden);
- toggleBtn.classList.toggle("active", isHidden);
- localStorage.setItem("obsigate-sidebar-hidden", isHidden ? "true" : "false");
- });
-}
-
-
-// ---------------------------------------------------------------------------
-// Mobile sidebar
-// ---------------------------------------------------------------------------
-export 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");
- });
-}
-
-export 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");
-}
-
-
-// ---------------------------------------------------------------------------
-// Resizable sidebar (horizontal)
-// ---------------------------------------------------------------------------
-export 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)
-// ---------------------------------------------------------------------------
-export 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);
- });
-}
-
-
-// ---------------------------------------------------------------------------
-// Frontmatter Accent Card Builder
-// ---------------------------------------------------------------------------
-
-// Persist toggle state across document openings
-let _frontmatterOpen = false;
-
-export 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}`;
- }
-
- // Extract boolean flags
- const booleanFlags = ["publish", "favoris", "template", "task", "archive", "draft", "private"].map((key) => ({ key, value: !!frontmatter[key] }));
-
- // Toggle state (persisted across documents)
- let isOpen = _frontmatterOpen;
-
- // Build header with chevron
- const chevron = el("span", { class: "fm-chevron " + (isOpen ? "open" : "closed") });
- chevron.innerHTML = ' ';
-
- const fmHeader = el("div", { class: "fm-header" }, [chevron, document.createTextNode("Frontmatter")]);
-
- // ZONE 1: Top strip
- const topBadges = [];
-
- // Title badge
- const title = frontmatter.titre || frontmatter.title || "";
- if (title) {
- topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)]));
- }
-
- // Status badge
- if (frontmatter.statut) {
- const statusBadge = el("span", { class: "ac-badge green" }, [el("span", { class: "ac-dot" }), document.createTextNode(frontmatter.statut)]);
- topBadges.push(statusBadge);
- }
-
- // Category badge
- if (frontmatter.catégorie || frontmatter.categorie) {
- const cat = frontmatter.catégorie || frontmatter.categorie;
- const catBadge = el("span", { class: "ac-badge blue" }, [document.createTextNode(cat)]);
- topBadges.push(catBadge);
- }
-
- // Publish badge
- if (frontmatter.publish) {
- topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("publié")]));
- }
-
- // Favoris badge
- if (frontmatter.favoris) {
- topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("favori")]));
- }
-
- const acTop = el("div", { class: "ac-top" }, topBadges);
-
- // ZONE 2: Body 2 columns
- const leftCol = el("div", { class: "ac-col" }, [
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("statut")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || "—")])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), el("span", { class: "ac-v muted" }, [document.createTextNode(frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(", ") : "[]")])]),
- ]);
-
- const rightCol = el("div", { class: "ac-col" }, [
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("publish")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])]),
- el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])]),
- ]);
-
- const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]);
-
- // ZONE 3: Tags row
- const tagPills = [];
- if (frontmatter.tags && frontmatter.tags.length > 0) {
- frontmatter.tags.forEach((tag) => {
- tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)]));
- });
- }
-
- const acTagsRow = el("div", { class: "ac-tags-row" }, [el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), el("div", { class: "ac-tags-wrap" }, tagPills)]);
-
- // ZONE 4: Flags row
- const flagChips = [];
- booleanFlags.forEach((flag) => {
- const chipClass = flag.value ? "flag-chip on" : "flag-chip off";
- flagChips.push(el("span", { class: chipClass }, [el("span", { class: "flag-dot" }), document.createTextNode(flag.key)]));
- });
-
- const acFlagsRow = el("div", { class: "ac-flags-row" }, [el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), ...flagChips]);
-
- // Assemble the card
- const acCard = el("div", { class: "ac-card" }, [acTop, acBody, acTagsRow, acFlagsRow]);
-
- // Set initial display based on persisted state
- if (!isOpen) acCard.style.display = "none";
-
- // Toggle functionality
- fmHeader.addEventListener("click", () => {
- isOpen = !isOpen;
- _frontmatterOpen = 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");
- }
- safeCreateIcons();
- });
-
- // Wrap in section
- const fmSection = el("div", { class: "fm-section" }, [fmHeader, acCard]);
-
- return fmSection;
-}
-
-
-// ---------------------------------------------------------------------------
-// File Operations Manager
-// ---------------------------------------------------------------------------
-
-const FileOperations = {
- showCreateDirectoryModal(vault, parentPath) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
-
- const input = modal.querySelector('#dir-name-input');
- const errorDiv = modal.querySelector('#dir-error');
- const createBtn = modal.querySelector('#dir-create-btn');
- const cancelBtn = modal.querySelector('#dir-cancel-btn');
-
- input.focus();
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- input.addEventListener('input', () => {
- const error = validateName(input.value);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- input.classList.add('error');
- } else {
- errorDiv.style.display = 'none';
- input.classList.remove('error');
- }
- });
-
- const create = async () => {
- const name = input.value.trim();
- const error = validateName(name);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- return;
- }
-
- const path = parentPath ? `${parentPath}/${name}` : name;
- createBtn.disabled = true;
- createBtn.textContent = t('modal.creating');
-
- try {
- await api(`/api/directory/${encodeURIComponent(vault)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path }),
- });
-
- showToast(t('toast.dir_created', {name: name}), 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
- } catch (err) {
- showToast(err.message || t('toast.create_error'), 'error');
- createBtn.disabled = false;
- createBtn.textContent = t('modal.create_btn');
- }
- };
-
- createBtn.addEventListener('click', create);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
-
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') create();
- if (e.key === 'Escape') this._closeModal(overlay);
- });
- },
-
- showCreateFileModal(vault, parentPath) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
- ${t('modal.file_type')}
-
- Markdown (.md)
- Texte (.txt)
- Python (.py)
- JavaScript (.js)
- JSON (.json)
- YAML (.yaml)
- Shell (.sh)
- PowerShell (.ps1)
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
-
- const input = modal.querySelector('#file-name-input');
- const extSelect = modal.querySelector('#file-ext-select');
- const errorDiv = modal.querySelector('#file-error');
- const createBtn = modal.querySelector('#file-create-btn');
- const cancelBtn = modal.querySelector('#file-cancel-btn');
-
- input.focus();
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- input.addEventListener('input', () => {
- const error = validateName(input.value);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- input.classList.add('error');
- } else {
- errorDiv.style.display = 'none';
- input.classList.remove('error');
- }
- });
-
- const create = async () => {
- let name = input.value.trim();
- const error = validateName(name);
- if (error) {
- errorDiv.textContent = error;
- errorDiv.style.display = 'block';
- return;
- }
-
- const ext = extSelect.value;
- if (!name.endsWith(ext)) {
- name += ext;
- }
-
- const path = parentPath ? `${parentPath}/${name}` : name;
- createBtn.disabled = true;
- createBtn.textContent = t('modal.creating');
-
- try {
- await api(`/api/file/${encodeURIComponent(vault)}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path, content: '' }),
- });
-
- showToast(t('toast.file_created', {name: name}), 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
- openFile(vault, path);
- } catch (err) {
- showToast(err.message || t('toast.create_error'), 'error');
- createBtn.disabled = false;
- createBtn.textContent = t('modal.create_btn');
- }
- };
-
- createBtn.addEventListener('click', create);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
-
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') create();
- if (e.key === 'Escape') this._closeModal(overlay);
- });
- },
-
- async startInlineRename(vault, path, type) {
- const item = document.querySelector(`.tree-item[data-vault="${CSS.escape(vault)}"][data-path="${CSS.escape(path)}"]`);
- if (!item) {
- showToast('Élément introuvable dans l’arborescence', 'error');
- return;
- }
-
- // Find the text span (.tree-item-text) — tree items use spans, not direct text nodes
- const textSpan = item.querySelector('.tree-item-text');
- if (!textSpan) {
- showToast(t('ui.cannot_rename'), 'error');
- return;
- }
-
- const originalText = textSpan.textContent;
- const trimmedOriginal = originalText.trim();
- const currentName = path.split('/').pop() || trimmedOriginal;
- const baseName = type === 'file' ? currentName.replace(/(\.[^./\\]+)$/i, '') : currentName;
- const extension = type === 'file' ? (currentName.match(/(\.[^./\\]+)$/i)?.[1] || '') : '';
-
- const input = document.createElement('input');
- input.type = 'text';
- input.className = 'sidebar-item-input';
- input.value = baseName;
-
- // Hide the text span, show input
- textSpan.style.display = 'none';
- textSpan.insertAdjacentElement('afterend', input);
-
- const restore = () => {
- input.remove();
- textSpan.style.display = '';
- };
-
- const validateName = (name) => {
- if (!name.trim()) return 'Le nom ne peut pas être vide';
- if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
- return null;
- };
-
- const submit = async () => {
- const name = input.value.trim();
- const error = validateName(name);
- if (error) {
- showToast(error, 'error');
- input.focus();
- input.select();
- return;
- }
-
- const newName = `${name}${extension}`;
- if (newName === currentName) {
- restore();
- return;
- }
-
- input.disabled = true;
- try {
- const endpoint = type === 'directory' ? `/api/directory/${encodeURIComponent(vault)}` : `/api/file/${encodeURIComponent(vault)}`;
- const result = await api(endpoint, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path, new_name: newName }),
- });
-
- const nextPath = result.new_path;
- await refreshSidebarTreePreservingState();
-
- if (type === 'file' && state.currentVault === vault && state.currentPath === path) {
- await openFile(vault, nextPath);
- } else if (type === 'directory' && state.currentVault === vault && state.currentPath && (state.currentPath === path || currentPath.startsWith(`${path}/`))) {
- const suffix = state.currentPath === path ? '' : currentPath.slice(path.length);
- state.currentPath = `${nextPath}${suffix}`;
- await focusPathInSidebar(vault, state.currentPath, { alignToTop: false });
- }
-
- showToast(type === 'directory' ? t('toast.dir_renamed') : t('toast.file_renamed'), 'success');
- } catch (err) {
- input.disabled = false;
- showToast(err.message || t('toast.rename_error'), 'error');
- input.focus();
- input.select();
- return;
- }
- };
-
- input.addEventListener('click', (e) => e.stopPropagation());
- input.addEventListener('keydown', async (e) => {
- e.stopPropagation();
- if (e.key === 'Enter') {
- e.preventDefault();
- await submit();
- }
- if (e.key === 'Escape') {
- e.preventDefault();
- restore();
- }
- });
- input.addEventListener('blur', async () => {
- if (!input.disabled) {
- await submit();
- }
- });
-
- input.focus();
- input.setSelectionRange(0, input.value.length);
- },
-
- confirmDeleteDirectory(vault, path) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
-
- ${t('modal.delete_warning')} ${t('modal.delete_irreversible')}
- ${t('modal.delete_dir_warning')}
-
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
- safeCreateIcons();
-
- const confirmBtn = modal.querySelector('#del-confirm-btn');
- const cancelBtn = modal.querySelector('#del-cancel-btn');
-
- const deleteDir = async () => {
- confirmBtn.disabled = true;
- confirmBtn.textContent = t('modal.creating');
-
- try {
- const result = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
- method: 'DELETE',
- });
-
- showToast(t('toast.dir_deleted', {count: result.deleted_count}), 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
-
- if (state.currentVault === vault && state.currentPath && currentPath.startsWith(path)) {
- showWelcome();
- }
- } catch (err) {
- showToast(err.message || t('toast.delete_error'), 'error');
- confirmBtn.disabled = false;
- confirmBtn.textContent = t('modal.delete_permanently');
- }
- };
-
- confirmBtn.addEventListener('click', deleteDir);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
- },
-
- confirmDeleteFile(vault, path) {
- const overlay = this._createModalOverlay();
- const modal = document.createElement('div');
- modal.className = 'obsigate-modal';
- modal.innerHTML = `
-
-
-
-
-
- ${t('modal.delete_warning')} ${t('modal.delete_irreversible')}
-
-
-
-
-
- `;
-
- overlay.appendChild(modal);
- document.body.appendChild(overlay);
-
- setTimeout(() => overlay.classList.add('active'), 10);
- safeCreateIcons();
-
- const confirmBtn = modal.querySelector('#del-confirm-btn');
- const cancelBtn = modal.querySelector('#del-cancel-btn');
-
- const deleteFile = async () => {
- confirmBtn.disabled = true;
- confirmBtn.textContent = t('modal.creating');
-
- try {
- await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
- method: 'DELETE',
- });
-
- showToast(t('toast.file_deleted'), 'success');
- this._closeModal(overlay);
- await refreshSidebarTreePreservingState();
-
- if (state.currentVault === vault && state.currentPath === path) {
- showWelcome();
- }
- } catch (err) {
- showToast(err.message || t('toast.delete_error'), 'error');
- confirmBtn.disabled = false;
- confirmBtn.textContent = t('modal.delete_permanently');
- }
- };
-
- confirmBtn.addEventListener('click', deleteFile);
- cancelBtn.addEventListener('click', () => this._closeModal(overlay));
- },
-
- _createModalOverlay() {
- const overlay = document.createElement('div');
- overlay.className = 'obsigate-modal-overlay';
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) {
- this._closeModal(overlay);
- }
- });
- return overlay;
- },
-
- _closeModal(overlay) {
- overlay.classList.remove('active');
- setTimeout(() => overlay.remove(), 200);
- }
-};
-
-
-// ---------------------------------------------------------------------------
-// Find in Page Manager
-// ---------------------------------------------------------------------------
-export const FindInPageManager = {
- isOpen: false,
- searchTerm: "",
- matches: [],
- currentIndex: -1,
- options: {
- caseSensitive: false,
- wholeWord: false,
- useRegex: false,
- },
- debounceTimer: null,
- previousFocus: null,
-
- init() {
- const bar = document.getElementById("find-in-page-bar");
- const input = document.getElementById("find-input");
- const prevBtn = document.getElementById("find-prev");
- const nextBtn = document.getElementById("find-next");
- const closeBtn = document.getElementById("find-close");
- const caseSensitiveBtn = document.getElementById("find-case-sensitive");
- const wholeWordBtn = document.getElementById("find-whole-word");
- const regexBtn = document.getElementById("find-regex");
-
- if (!bar || !input) return;
-
- // Keyboard shortcuts
- document.addEventListener("keydown", (e) => {
- // Ctrl+F or Cmd+F to open
- if ((e.ctrlKey || e.metaKey) && e.key === "f") {
- e.preventDefault();
- this.open();
- }
- // Escape to close
- if (e.key === "Escape" && this.isOpen) {
- e.preventDefault();
- this.close();
- }
- // Enter to go to next
- if (e.key === "Enter" && this.isOpen && document.activeElement === input) {
- e.preventDefault();
- if (e.shiftKey) {
- this.goToPrevious();
- } else {
- this.goToNext();
- }
- }
- // F3 for next/previous
- if (e.key === "F3" && this.isOpen) {
- e.preventDefault();
- if (e.shiftKey) {
- this.goToPrevious();
- } else {
- this.goToNext();
- }
- }
- });
-
- // Input event with debounce
- input.addEventListener("input", (e) => {
- clearTimeout(this.debounceTimer);
- this.debounceTimer = setTimeout(() => {
- this.search(e.target.value);
- }, 250);
- });
-
- // Navigation buttons
- prevBtn.addEventListener("click", () => this.goToPrevious());
- nextBtn.addEventListener("click", () => this.goToNext());
-
- // Close button
- closeBtn.addEventListener("click", () => this.close());
-
- // Option toggles
- caseSensitiveBtn.addEventListener("click", () => {
- this.options.caseSensitive = !this.options.caseSensitive;
- caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- wholeWordBtn.addEventListener("click", () => {
- this.options.wholeWord = !this.options.wholeWord;
- wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- regexBtn.addEventListener("click", () => {
- this.options.useRegex = !this.options.useRegex;
- regexBtn.setAttribute("aria-pressed", this.options.useRegex);
- this.saveState();
- if (this.searchTerm) this.search(this.searchTerm);
- });
-
- // Load saved state
- this.loadState();
- },
-
- open() {
- const bar = document.getElementById("find-in-page-bar");
- const input = document.getElementById("find-input");
- if (!bar || !input) return;
-
- this.previousFocus = document.activeElement;
- this.isOpen = true;
- bar.hidden = false;
- input.focus();
- input.select();
- safeCreateIcons();
- },
-
- close() {
- const bar = document.getElementById("find-in-page-bar");
- if (!bar) return;
-
- this.isOpen = false;
- bar.hidden = true;
- this.clearHighlights();
- this.matches = [];
- this.currentIndex = -1;
- this.searchTerm = "";
-
- // Restore previous focus
- if (this.previousFocus && this.previousFocus.focus) {
- this.previousFocus.focus();
- }
- },
-
- search(term) {
- this.searchTerm = term;
- this.clearHighlights();
- this.hideError();
-
- if (!term || term.trim().length === 0) {
- this.updateCounter();
- this.updateNavButtons();
- return;
- }
-
- const contentArea = document.querySelector(".md-content");
- if (!contentArea) {
- this.updateCounter();
- this.updateNavButtons();
- return;
- }
-
- try {
- const regex = this.createRegex(term);
- this.matches = [];
- this.findMatches(contentArea, regex);
- this.currentIndex = this.matches.length > 0 ? 0 : -1;
- this.highlightMatches();
- this.updateCounter();
- this.updateNavButtons();
-
- if (this.matches.length > 0) {
- this.scrollToMatch(0);
- }
- } catch (err) {
- this.showError(err.message);
- this.matches = [];
- this.currentIndex = -1;
- this.updateCounter();
- this.updateNavButtons();
- }
- },
-
- createRegex(term) {
- let pattern = term;
-
- if (!this.options.useRegex) {
- // Escape special regex characters
- pattern = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- }
-
- if (this.options.wholeWord) {
- pattern = "\\b" + pattern + "\\b";
- }
-
- const flags = this.options.caseSensitive ? "g" : "gi";
- return new RegExp(pattern, flags);
- },
-
- findMatches(container, regex) {
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
- acceptNode: (node) => {
- // Skip code blocks, scripts, styles
- const parent = node.parentElement;
- if (!parent) return NodeFilter.FILTER_REJECT;
- const tagName = parent.tagName.toLowerCase();
- if (["code", "pre", "script", "style"].includes(tagName)) {
- return NodeFilter.FILTER_REJECT;
- }
- // Skip empty text nodes
- if (!node.textContent || node.textContent.trim().length === 0) {
- return NodeFilter.FILTER_REJECT;
- }
- return NodeFilter.FILTER_ACCEPT;
- },
- });
-
- let node;
- while ((node = walker.nextNode())) {
- const text = node.textContent;
- let match;
- regex.lastIndex = 0; // Reset regex
-
- while ((match = regex.exec(text)) !== null) {
- this.matches.push({
- node: node,
- index: match.index,
- length: match[0].length,
- text: match[0],
- });
-
- // Prevent infinite loop with zero-width matches
- if (match.index === regex.lastIndex) {
- regex.lastIndex++;
- }
- }
- }
- },
-
- highlightMatches() {
- const matchesByNode = new Map();
-
- this.matches.forEach((match, idx) => {
- if (!matchesByNode.has(match.node)) {
- matchesByNode.set(match.node, []);
- }
- matchesByNode.get(match.node).push({ match, idx });
- });
-
- matchesByNode.forEach((entries, node) => {
- if (!node || !node.parentNode) return;
-
- const text = node.textContent || "";
- let cursor = 0;
- const fragment = document.createDocumentFragment();
-
- entries.sort((a, b) => a.match.index - b.match.index);
-
- entries.forEach(({ match, idx }) => {
- if (match.index > cursor) {
- fragment.appendChild(document.createTextNode(text.substring(cursor, match.index)));
- }
-
- const matchText = text.substring(match.index, match.index + match.length);
- const mark = document.createElement("mark");
- mark.className = idx === this.currentIndex ? "find-highlight find-highlight-active" : "find-highlight";
- mark.textContent = matchText;
- mark.setAttribute("data-find-index", idx);
- fragment.appendChild(mark);
-
- match.element = mark;
- cursor = match.index + match.length;
- });
-
- if (cursor < text.length) {
- fragment.appendChild(document.createTextNode(text.substring(cursor)));
- }
-
- node.parentNode.replaceChild(fragment, node);
- });
- },
-
- clearHighlights() {
- const contentArea = document.querySelector(".md-content");
- if (!contentArea) return;
-
- const marks = contentArea.querySelectorAll("mark.find-highlight");
- marks.forEach((mark) => {
- if (!mark.parentNode) return;
- const text = mark.textContent;
- const textNode = document.createTextNode(text);
- mark.parentNode.replaceChild(textNode, mark);
- });
-
- // Normalize text nodes to merge adjacent text nodes
- contentArea.normalize();
- },
-
- goToNext() {
- if (this.matches.length === 0) return;
-
- // Remove active class from current
- if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.remove("find-highlight-active");
- }
-
- // Move to next (with wrapping)
- this.currentIndex = (this.currentIndex + 1) % this.matches.length;
-
- // Add active class to new current
- if (this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.add("find-highlight-active");
- }
-
- this.scrollToMatch(this.currentIndex);
- this.updateCounter();
- },
-
- goToPrevious() {
- if (this.matches.length === 0) return;
-
- // Remove active class from current
- if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.remove("find-highlight-active");
- }
-
- // Move to previous (with wrapping)
- this.currentIndex = this.currentIndex <= 0 ? this.matches.length - 1 : this.currentIndex - 1;
-
- // Add active class to new current
- if (this.matches[this.currentIndex].element) {
- this.matches[this.currentIndex].element.classList.add("find-highlight-active");
- }
-
- this.scrollToMatch(this.currentIndex);
- this.updateCounter();
- },
-
- scrollToMatch(index) {
- if (index < 0 || index >= this.matches.length) return;
-
- const match = this.matches[index];
- if (!match.element) return;
-
- const contentArea = document.getElementById("content-area");
- if (!contentArea) {
- match.element.scrollIntoView({ behavior: "smooth", block: "center" });
- return;
- }
-
- // Calculate position with offset for header
- const elementTop = match.element.offsetTop;
- const offset = 100; // Offset for header
-
- contentArea.scrollTo({
- top: elementTop - offset,
- behavior: "smooth",
- });
- },
-
- updateCounter() {
- const counter = document.getElementById("find-counter");
- if (!counter) return;
-
- const count = this.matches.length;
- if (count === 0) {
- counter.textContent = "0 occurrence";
- } else if (count === 1) {
- counter.textContent = "1 occurrence";
- } else {
- counter.textContent = `${count} occurrences`;
- }
- },
-
- updateNavButtons() {
- const prevBtn = document.getElementById("find-prev");
- const nextBtn = document.getElementById("find-next");
- if (!prevBtn || !nextBtn) return;
-
- const hasMatches = this.matches.length > 0;
- prevBtn.disabled = !hasMatches;
- nextBtn.disabled = !hasMatches;
- },
-
- showError(message) {
- const errorEl = document.getElementById("find-error");
- if (!errorEl) return;
-
- errorEl.textContent = message;
- errorEl.hidden = false;
- },
-
- hideError() {
- const errorEl = document.getElementById("find-error");
- if (!errorEl) return;
-
- errorEl.hidden = true;
- },
-
- saveState() {
- try {
- const state = {
- options: this.options,
- };
- localStorage.setItem("obsigate-find-in-page-state", JSON.stringify(state));
- } catch (e) {
- // Ignore localStorage errors
- }
- },
-
- loadState() {
- try {
- const saved = localStorage.getItem("obsigate-find-in-page-state");
- if (saved) {
- const state = JSON.parse(saved);
- if (state.options) {
- this.options = { ...this.options, ...state.options };
-
- // Update button states
- const caseSensitiveBtn = document.getElementById("find-case-sensitive");
- const wholeWordBtn = document.getElementById("find-whole-word");
- const regexBtn = document.getElementById("find-regex");
-
- if (caseSensitiveBtn) caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive);
- if (wholeWordBtn) wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord);
- if (regexBtn) regexBtn.setAttribute("aria-pressed", this.options.useRegex);
- }
- }
- } catch (e) {
- // Ignore localStorage errors
- }
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Init
-// ---------------------------------------------------------------------------
-async function init() {
- initTheme();
- initHeaderMenu();
- initCustomDropdowns();
- document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
- document.getElementById("header-logo").addEventListener("click", goHome);
- const refreshBtn = document.getElementById("header-refresh-btn");
- if (refreshBtn) refreshBtn.addEventListener("click", goHome);
- initSearch();
- initSidebarToggle();
- initMobile();
- initVaultContext();
- initSidebarTabs();
- initHelpModal();
- initConfigModal();
- initSidebarFilter();
- initSidebarResize();
- initEditor();
- initLoginForm();
- initRecentTab();
- RightSidebarManager.init();
- FindInPageManager.init();
- ContextMenuManager.init();
-
- // Check auth status first
- const authOk = await AuthManager.initAuth();
-
- if (authOk) {
- // Start SSE sync AFTER auth is established (cookie available)
- initSyncStatus();
-
- try {
- await Promise.all([loadVaultSettings(), loadVaults(), loadTags()]);
-
- // Initialize dashboard widgets now that vaults are loaded
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.init();
- }
-
- // Check for popup mode query parameter
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get("popup") === "true") {
- document.body.classList.add("popup-mode");
- }
-
- // Handle direct deep-link to file via #file=...
- if (window.location.hash && window.location.hash.startsWith("#file=")) {
- const hashVal = window.location.hash.substring(6);
- const sepIndex = hashVal.indexOf(":");
- if (sepIndex > -1) {
- const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
- const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
- openFile(vault, path);
- }
- } else if (urlParams.get("popup") !== "true") {
- // Default to dashboard if no deep link and not in popup mode
- showWelcome();
- }
- } catch (err) {
- console.error("Failed to initialize ObsiGate:", err);
- showToast(t("error.initialization"), "error");
- }
- }
-
- safeCreateIcons();
-}
-
-
-
-// ---- Keyboard shortcuts for tabs ----
-document.addEventListener("keydown", (e) => {
- if (e.ctrlKey || e.metaKey) {
- if (e.key === "w" || e.key === "W") {
- e.preventDefault();
- if (TabManager._activeTabId) {
- TabManager.close(TabManager._activeTabId);
- }
- } else if (e.key === "Tab" && !e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const nextIdx = (currentIdx + 1) % tabs.length;
- TabManager.activate(tabs[nextIdx].id);
- }
- } else if (e.key === "Tab" && e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length;
- TabManager.activate(tabs[prevIdx].id);
- }
- }
- }
-});
-
-
-
-
-// ===== MISSING MANAGERS (extracted separately) =====
-
-export const ContextMenuManager = {
- _menu: null,
- _targetElement: null,
- _targetVault: null,
- _targetPath: null,
- _targetType: null,
-
- init() {
- this._menu = document.createElement('div');
- this._menu.className = 'context-menu';
- this._menu.id = 'context-menu';
- document.body.appendChild(this._menu);
-
- document.addEventListener('click', () => this.hide());
- document.addEventListener('contextmenu', (e) => {
- if (!e.target.closest('.tree-item')) {
- this.hide();
- }
- });
-
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') this.hide();
- });
-
- document.addEventListener('scroll', () => this.hide(), true);
- },
-
- show(x, y, vault, path, type, isReadonly) {
- this._targetVault = vault;
- this._targetPath = path;
- this._targetType = type;
-
- this._menu.innerHTML = '';
-
- // Copy path — available for all types
- const pathToCopy = type === 'vault' ? vault : `${vault}/${path}`;
- this._addItem('clipboard-copy', 'Copier le chemin', () => this._copyPath(pathToCopy), false);
-
- // Graph view — available for all types
- const graphPath = type === 'vault' ? '' : path;
- this._addItem('git-graph', 'Vue Graphique', () => GraphViewManager.open(vault, graphPath, type), false);
-
- this._addSeparator();
-
- if (type === 'vault') {
- this._addItem('folder-plus', 'Nouveau dossier', () => this._createDirectory(), isReadonly);
- this._addItem('file-plus', 'Nouveau fichier', () => this._createFile(), isReadonly);
- } else if (type === 'directory') {
- this._addItem('folder-plus', 'Nouveau sous-dossier', () => this._createDirectory(), isReadonly);
- this._addItem('file-plus', 'Nouveau fichier ici', () => this._createFile(), isReadonly);
- this._addSeparator();
- this._addItem('bookmark-plus', 'Ajouter aux recherches sauvegardees', () => this._saveDirectorySearch(), false);
- this._addSeparator();
- this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
- this._addItem('trash-2', 'Supprimer', () => this._deleteDirectory(), isReadonly);
- } else if (type === 'file') {
- this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
- this._addItem('trash-2', 'Supprimer', () => this._deleteFile(), isReadonly);
- this._addSeparator();
- this._addItem('bookmark-plus', t("viewer.bookmark_add"), () => this._toggleBookmark(), false);
- this._addSeparator();
- this._addItem('history', t("ui.backups_versions"), () => this._openBackupViewer(), false);
- }
-
- this._menu.classList.add('active');
-
- const rect = this._menu.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- let finalX = x;
- let finalY = y;
-
- if (x + rect.width > viewportWidth) {
- finalX = viewportWidth - rect.width - 10;
- }
-
- if (y + rect.height > viewportHeight) {
- finalY = viewportHeight - rect.height - 10;
- }
-
- this._menu.style.left = `${finalX}px`;
- this._menu.style.top = `${finalY}px`;
-
- safeCreateIcons();
- },
-
- hide() {
- if (this._menu) {
- this._menu.classList.remove('active');
- }
- },
-
- _addItem(icon, label, callback, disabled) {
- const item = document.createElement('div');
- item.className = 'context-menu-item' + (disabled ? ' disabled' : '');
- item.innerHTML = `
-
- ${label}
- `;
-
- if (!disabled) {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- this.hide();
- callback();
- });
- } else {
- item.title = t("ui.vault_readonly");
- }
-
- this._menu.appendChild(item);
- },
-
- _addSeparator() {
- const sep = document.createElement('div');
- sep.className = 'context-menu-separator';
- this._menu.appendChild(sep);
- },
-
- _createDirectory() {
- FileOperations.showCreateDirectoryModal(this._targetVault, this._targetPath);
- },
-
- _createFile() {
- FileOperations.showCreateFileModal(this._targetVault, this._targetPath);
- },
-
- _renameItem() {
- FileOperations.startInlineRename(this._targetVault, this._targetPath, this._targetType);
- },
-
- _deleteDirectory() {
- FileOperations.confirmDeleteDirectory(this._targetVault, this._targetPath);
- },
-
- _deleteFile() {
- FileOperations.confirmDeleteFile(this._targetVault, this._targetPath);
- },
-
- _copyPath(path) {
- // Try modern clipboard API first, fall back to execCommand for non-secure contexts
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(path).then(() => {
- showToast(t('toast.path_copied', {path: path}), 'success');
- }).catch(() => {
- this._copyPathFallback(path);
- });
- } else {
- this._copyPathFallback(path);
- }
- },
-
- _copyPathFallback(path) {
- const textarea = document.createElement('textarea');
- textarea.value = path;
- textarea.style.position = 'fixed';
- textarea.style.left = '-9999px';
- textarea.style.top = '-9999px';
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
- try {
- const success = document.execCommand('copy');
- if (success) {
- showToast(t('toast.path_copied', {path: path}), 'success');
- } else {
- showToast(t('toast.copy_error'), 'error');
- }
- } catch (e) {
- showToast(t('toast.copy_error'), 'error');
- }
- document.body.removeChild(textarea);
- },
-
- async _toggleBookmark() {
- try {
- const data = await api("/api/bookmarks/toggle", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ vault: this._targetVault, path: this._targetPath, title: this._targetPath.split("/").pop() }),
- });
- showToast(data.bookmarked ? t("toast.bookmark_added") : t("toast.bookmark_removed"), "success");
- if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
- DashboardBookmarkWidget.load();
- }
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- },
-
- async _saveDirectorySearch() {
- try {
- var vault = this._targetVault || "all";
- var dirPath = this._targetPath;
- var dirName = dirPath.split("/").pop() || dirPath;
- await api("/api/saved-searches", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- query: "",
- vault: vault,
- case_sensitive: false,
- whole_word: false,
- regex: false,
- include_paths: dirPath + "/**",
- exclude_paths: "",
- }),
- });
- showToast(t("toast.dir_saved_search"), "success");
- // Switch to saved searches tab
- var savedTab = document.getElementById("sidebar-tab-saved");
- if (savedTab) savedTab.click();
- // Reload saved searches
- import('./viewer.js').then(function(m) { m.loadSavedSearches(); });
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- },
-
- _openBackupViewer() {
- // Dynamically import the backup viewer module to avoid circular deps
- import('./backups.js').then(mod => {
- mod.openBackupViewer(this._targetVault, this._targetPath);
- }).catch(err => {
- showToast(t("toast.viewer_error") + ": " + err.message, "error");
- });
- this.hide();
- }
-};
-export const TabManager = {
- _tabs: [],
- _activeTabId: null,
- _previewTabId: null, // single-click preview tab (temporary, replaced on next preview)
- _tabCache: {}, // { tabId: { vault, path, title, data, rawSource, sourceView, scrollTop, icon } }
- _tabBar: null,
- _tabList: null,
- _dirtyTabs: new Set(),
-
- init() {
- this._tabBar = document.getElementById("tab-bar");
- this._tabList = document.getElementById("tab-list");
- },
-
- /** Open a file as a preview tab (single-click).
- * Replaces any existing preview tab. If the file is already
- * open as a persistent tab, just activates it. */
- async openPreview(vault, path) {
- const tabId = `${vault}::${path}`;
-
- // If already open as persistent tab, just activate it
- const existing = this._tabs.find(t => t.id === tabId && !t.preview);
- if (existing) {
- this.activate(tabId);
- return;
- }
-
- // Close existing preview tab
- if (this._previewTabId && this._previewTabId !== tabId) {
- this.close(this._previewTabId);
- }
-
- // If already open as preview, just focus it
- const previewExisting = this._tabs.find(t => t.id === tabId && t.preview);
- if (previewExisting) {
- this.activate(tabId);
- return;
- }
-
- // Create preview tab
- const name = path.split("/").pop().replace(/\.md$/i, "");
- const icon = getFileIcon(name + ".md");
-
- this._tabs.push({ id: tabId, vault, path, name, icon, preview: true });
- this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
- this._previewTabId = tabId;
-
- this._renderTabs();
- this.activate(tabId);
- },
-
- /** Convert a preview tab to a persistent tab (double-click).
- * If already persistent, opens a new duplicate (same file, different tab). */
- async openPersistent(vault, path) {
- const tabId = `${vault}::${path}`;
-
- // If it's already a preview tab, convert it to persistent
- const previewTab = this._tabs.find(t => t.id === tabId && t.preview);
- if (previewTab) {
- previewTab.preview = false;
- if (this._previewTabId === tabId) {
- this._previewTabId = null;
- }
- this._renderTabs();
- this.activate(tabId);
- return;
- }
-
- // If already persistent, just focus it
- const existing = this._tabs.find(t => t.id === tabId && !t.preview);
- if (existing) {
- this.activate(tabId);
- return;
- }
-
- // Create a new persistent tab
- this.open(vault, path);
- },
-
- /** Open a file in a tab (or focus existing) */
- async open(vault, path, options = {}) {
- const tabId = `${vault}::${path}`;
-
- // If already open, just focus it
- const existing = this._tabs.find(t => t.id === tabId);
- if (existing) {
- // Convert preview to persistent if needed
- if (existing.preview) {
- existing.preview = false;
- if (this._previewTabId === tabId) this._previewTabId = null;
- this._renderTabs();
- }
- this.activate(tabId);
- return;
- }
-
- // Create new tab
- const name = path.split("/").pop().replace(/\.md$/i, "");
- const icon = getFileIcon(name + ".md");
-
- this._tabs.push({ id: tabId, vault, path, name, icon });
- this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
-
- this._renderTabs();
- this.activate(tabId);
- },
-
- /** Activate a specific tab */
- async activate(tabId) {
- if (this._activeTabId === tabId && this._tabs.length > 0) return;
-
- // Save current tab state
- if (this._activeTabId && this._tabCache[this._activeTabId]) {
- this._saveCurrentTabState();
- }
-
- this._activeTabId = tabId;
- this._renderTabs();
-
- // Load tab content
- const cache = this._tabCache[tabId];
- if (!cache) return;
-
- // Update global state
- state.currentVault = cache.vault;
- state.currentPath = cache.path;
- syncActiveFileTreeItem(cache.vault, cache.path);
-
- const area = document.getElementById("content-area");
-
- if (cache.data) {
- // Use cached data
- this._restoreTabContent(cache, area);
- } else {
- // Fetch file content
- area.innerHTML = 'Chargement...
';
- try {
- const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`);
- cache.data = data;
- cache.title = data.title;
- renderFile(cache.data);
-
- // Restore source view if needed
- if (cache.sourceView) {
- await this._toggleSourceView(cache, area);
- }
- if (cache.scrollTop) {
- area.scrollTop = cache.scrollTop;
- }
- } catch (err) {
- area.innerHTML = `Erreur: ${escapeHtml(err.message)}
`;
- }
- }
-
- // Update URL hash
- if (history.pushState) {
- history.pushState(null, "", `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`);
- }
-
- // Hide dashboard
- const dashboard = document.getElementById("dashboard-home");
- if (dashboard) dashboard.style.display = "none";
- },
-
- /** Close a tab */
- close(tabId) {
- const idx = this._tabs.findIndex(t => t.id === tabId);
- if (idx === -1) return;
-
- this._tabs.splice(idx, 1);
- delete this._tabCache[tabId];
- this._dirtyTabs.delete(tabId);
-
- if (this._tabs.length === 0) {
- this._activeTabId = null;
- this._showDashboard();
- if (this._tabBar) this._tabBar.hidden = true;
- } else if (this._activeTabId === tabId) {
- // Activate adjacent tab
- const newIdx = Math.min(idx, this._tabs.length - 1);
- this.activate(this._tabs[newIdx].id);
- }
-
- this._renderTabs();
- },
-
- /** Close all tabs */
- closeAll() {
- this._tabs = [];
- this._tabCache = {};
- this._dirtyTabs.clear();
- this._activeTabId = null;
- this._showDashboard();
- if (this._tabBar) this._tabBar.hidden = true;
- },
-
- /** Close tabs to the right */
- closeRight(tabId) {
- const idx = this._tabs.findIndex(t => t.id === tabId);
- if (idx === -1) return;
- const toClose = this._tabs.slice(idx + 1);
- for (const tab of toClose) {
- delete this._tabCache[tab.id];
- this._dirtyTabs.delete(tab.id);
- }
- this._tabs = this._tabs.slice(0, idx + 1);
- if (!this._tabs.find(t => t.id === this._activeTabId)) {
- this.activate(tabId);
- }
- this._renderTabs();
- },
-
- /** Close other tabs */
- closeOthers(tabId) {
- const tab = this._tabs.find(t => t.id === tabId);
- if (!tab) return;
- for (const t of this._tabs) {
- if (t.id !== tabId) {
- delete this._tabCache[t.id];
- this._dirtyTabs.delete(t.id);
- }
- }
- this._tabs = [tab];
- this.activate(tabId);
- this._renderTabs();
- },
-
- /** Reorder tabs by drag and drop */
- moveTab(fromIdx, toIdx) {
- if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return;
- const tab = this._tabs.splice(fromIdx, 1)[0];
- this._tabs.splice(toIdx, 0, tab);
- this._renderTabs();
- },
-
- /** Save current tab state before switching */
- _saveCurrentTabState() {
- const cache = this._tabCache[this._activeTabId];
- if (!cache) return;
-
- const area = document.getElementById("content-area");
- const rendered = document.getElementById("file-rendered-content");
-
- cache.scrollTop = area.scrollTop;
- cache.sourceView = rendered ? rendered.style.display === "none" : false;
- },
-
- /** Restore tab content from cache */
- _restoreTabContent(cache, area) {
- renderFile(cache.data);
- if (cache.sourceView) {
- this._restoreSourceView(cache, area);
- }
- if (cache.scrollTop) {
- area.scrollTop = cache.scrollTop;
- }
- },
-
- async _toggleSourceView(cache, area) {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (!rendered || !raw) return;
-
- if (!cache.rawSource) {
- const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`);
- cache.rawSource = rawData.raw;
- }
- raw.textContent = cache.rawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- },
-
- _restoreSourceView(cache, area) {
- requestAnimationFrame(() => {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (rendered && raw && cache.rawSource) {
- raw.textContent = cache.rawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- }
- });
- },
-
- _showDashboard() {
- const area = document.getElementById("content-area");
- // Save dashboard DOM before clearing (it may have been removed from DOM by renderFile)
- let dashboard = document.getElementById("dashboard-home");
- if (!dashboard) {
- // Dashboard was destroyed — rebuild via showWelcome
- area.innerHTML = "";
- showWelcome();
- return;
- }
- area.innerHTML = "";
- dashboard.style.display = "";
- area.appendChild(dashboard);
- // Refresh widgets after restoring
- if (typeof DashboardStatsWidget !== "undefined") DashboardStatsWidget.load();
- if (typeof DashboardConflictsWidget !== "undefined") DashboardConflictsWidget.load();
- if (typeof DashboardRecentWidget !== "undefined") DashboardRecentWidget.load(state.selectedContextVault);
- if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load(state.selectedContextVault);
- if (history.pushState) {
- history.pushState(null, "", "#");
- }
- },
-
- /** Render the tab bar */
- _renderTabs() {
- if (!this._tabList) return;
-
- this._tabList.innerHTML = "";
-
- if (this._tabs.length === 0) {
- this._tabBar.hidden = true;
- return;
- }
-
- this._tabBar.hidden = false;
-
- this._tabs.forEach((tab, idx) => {
- const el = document.createElement("div");
- el.className = "tab-item" + (tab.id === this._activeTabId ? " active" : "") + (tab.preview ? " preview" : "");
- el.draggable = true;
- el.dataset.tabId = tab.id;
- el.dataset.index = idx;
-
- // Icon
- const iconEl = document.createElement("i");
- iconEl.setAttribute("data-lucide", tab.icon);
- iconEl.className = "tab-icon";
- iconEl.style.width = "14px";
- iconEl.style.height = "14px";
- el.appendChild(iconEl);
-
- // Name
- const nameEl = document.createElement("span");
- nameEl.className = "tab-name";
- nameEl.textContent = tab.name;
- nameEl.title = `${tab.vault}/${tab.path}`;
- el.appendChild(nameEl);
-
- // Close button
- const closeEl = document.createElement("span");
- closeEl.className = "tab-close";
- closeEl.innerHTML = ' ';
- closeEl.addEventListener("click", (e) => {
- e.stopPropagation();
- this.close(tab.id);
- });
- el.appendChild(closeEl);
-
- // Click to activate
- el.addEventListener("click", () => this.activate(tab.id));
-
- // Double-click to close
- el.addEventListener("dblclick", (e) => {
- e.preventDefault();
- this.close(tab.id);
- });
-
- // Middle-click to close
- el.addEventListener("mousedown", (e) => {
- if (e.button === 1) {
- e.preventDefault();
- this.close(tab.id);
- }
- });
-
- // Context menu on tab
- el.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- this._showTabContextMenu(e.clientX, e.clientY, tab.id);
- });
-
- // Drag and drop
- el.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", String(idx));
- el.classList.add("dragging");
- });
- el.addEventListener("dragend", () => {
- el.classList.remove("dragging");
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- });
- el.addEventListener("dragover", (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- const rect = el.getBoundingClientRect();
- const mid = rect.left + rect.width / 2;
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- const indicator = document.createElement("div");
- indicator.className = "tab-drop-indicator";
- if (e.clientX < mid) {
- el.before(indicator);
- } else {
- el.after(indicator);
- }
- });
- el.addEventListener("drop", (e) => {
- e.preventDefault();
- document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
- const fromIdx = parseInt(e.dataTransfer.getData("text/plain"));
- const rect = el.getBoundingClientRect();
- const mid = rect.left + rect.width / 2;
- const toIdx = e.clientX < mid ? idx : idx + 1;
- if (fromIdx !== toIdx && fromIdx !== toIdx - 1) {
- this.moveTab(fromIdx, toIdx);
- }
- });
-
- this._tabList.appendChild(el);
- });
-
- safeCreateIcons();
- },
-
- _showTabContextMenu(x, y, tabId) {
- const existing = document.getElementById("tab-context-menu");
- if (existing) existing.remove();
-
- const menu = document.createElement("div");
- menu.id = "tab-context-menu";
- menu.className = "context-menu active";
- menu.style.left = x + "px";
- menu.style.top = y + "px";
- menu.innerHTML = `
-
-
-
-
-
- `;
- document.body.appendChild(menu);
- safeCreateIcons();
-
- menu.addEventListener("click", (e) => {
- const action = e.target.closest(".context-menu-item")?.dataset.action;
- if (action === "close") this.close(tabId);
- else if (action === "closeOthers") this.closeOthers(tabId);
- else if (action === "closeRight") this.closeRight(tabId);
- else if (action === "closeAll") this.closeAll();
- menu.remove();
- });
-
- const closeMenu = () => menu.remove();
- document.addEventListener("click", closeMenu, { once: true });
- document.addEventListener("keydown", (e) => { if (e.key === "Escape") { menu.remove(); } }, { once: true });
- },
-};
-
-
-
-// ---- Keyboard shortcuts for tabs ----
-document.addEventListener("keydown", (e) => {
- if (e.ctrlKey || e.metaKey) {
- if (e.key === "w" || e.key === "W") {
- e.preventDefault();
- if (TabManager._activeTabId) {
- TabManager.close(TabManager._activeTabId);
- }
- } else if (e.key === "Tab" && !e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const nextIdx = (currentIdx + 1) % tabs.length;
- TabManager.activate(tabs[nextIdx].id);
- }
- } else if (e.key === "Tab" && e.shiftKey) {
- e.preventDefault();
- const tabs = TabManager._tabs;
- const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
- if (currentIdx >= 0 && tabs.length > 1) {
- const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length;
- TabManager.activate(tabs[prevIdx].id);
- }
- }
- }
-});
-
-// ---- Modify init to include TabManager ----
diff --git a/desktop/frontend-static/js/utils.js b/desktop/frontend-static/js/utils.js
deleted file mode 100644
index b5ab338..0000000
--- a/desktop/frontend-static/js/utils.js
+++ /dev/null
@@ -1,679 +0,0 @@
-import { state } from './state.js';
-import { api } from './auth.js';
-import { openFile, showWelcome } from './viewer.js';
-import { refreshSidebarForContext, refreshTagsForContext } from './sidebar.js';
-import { createAIToolbar } from './ai.js';
-
-// ---------------------------------------------------------------------------
-// File extension → Lucide icon mapping
-// ---------------------------------------------------------------------------
-const EXT_ICONS = {
- // Text files
- ".md": "file-text",
- ".txt": "file-text",
- ".log": "file-text",
- ".readme": "file-text",
- ".rst": "file-text",
- ".adoc": "file-text",
-
- // Web development
- ".html": "file-code",
- ".htm": "file-code",
- ".css": "file-code",
- ".scss": "file-code",
- ".sass": "file-code",
- ".less": "file-code",
- ".js": "file-code",
- ".jsx": "file-code",
- ".ts": "file-code",
- ".tsx": "file-code",
- ".vue": "file-code",
- ".svelte": "file-code",
-
- // Programming languages
- ".py": "file-code",
- ".java": "file-code",
- ".c": "file-code",
- ".cpp": "file-code",
- ".cc": "file-code",
- ".cxx": "file-code",
- ".h": "file-code",
- ".hpp": "file-code",
- ".cs": "file-code",
- ".go": "file-code",
- ".rs": "file-code",
- ".rb": "file-code",
- ".php": "file-code",
- ".swift": "file-code",
- ".kt": "file-code",
- ".scala": "file-code",
- ".r": "file-code",
- ".m": "file-code",
- ".pl": "file-code",
- ".lua": "file-code",
- ".dart": "file-code",
- ".nim": "file-code",
- ".zig": "file-code",
- ".odin": "file-code",
- ".v": "file-code",
- ".cr": "file-code",
- ".ex": "file-code",
- ".exs": "file-code",
- ".elm": "file-code",
- ".purs": "file-code",
- ".hs": "file-code",
- ".ml": "file-code",
- ".ocaml": "file-code",
- ".fs": "file-code",
- ".fsx": "file-code",
- ".vb": "file-code",
- ".pas": "file-code",
- ".pp": "file-code",
- ".inc": "file-code",
-
- // Data formats
- ".json": "file-json",
- ".yaml": "file-cog",
- ".yml": "file-cog",
- ".toml": "file-cog",
- ".xml": "file-code",
- ".csv": "table",
- ".tsv": "table",
- ".sql": "database",
- ".db": "database",
- ".sqlite": "database",
- ".sqlite3": "database",
- ".parquet": "database",
- ".avro": "database",
-
- // Configuration files
- ".ini": "file-cog",
- ".cfg": "file-cog",
- ".conf": "file-cog",
- ".env": "file-cog",
- ".dockerfile": "file-cog",
- ".gitignore": "file-cog",
- ".gitattributes": "file-cog",
- ".editorconfig": "file-cog",
- ".eslintrc": "file-cog",
- ".prettierrc": "file-cog",
- ".babelrc": "file-cog",
- ".tsconfig": "file-cog",
- "package.json": "file-cog",
- "package-lock.json": "file-cog",
- "yarn.lock": "file-cog",
- "composer.json": "file-cog",
- "requirements.txt": "file-cog",
- "pipfile": "file-cog",
- "gemfile": "file-cog",
- "cargo.toml": "file-cog",
- "go.mod": "file-cog",
- "go.sum": "file-cog",
- "pom.xml": "file-cog",
- "build.gradle": "file-cog",
- "cmakelists.txt": "file-cog",
- "makefile": "file-cog",
-
- // Shell scripts
- ".sh": "terminal",
- ".bash": "terminal",
- ".zsh": "terminal",
- ".fish": "terminal",
- ".bat": "terminal",
- ".cmd": "terminal",
- ".ps1": "terminal",
- ".psm1": "terminal",
- ".psd1": "terminal",
-
- // Document formats
- ".pdf": "file-text",
- ".doc": "file-text",
- ".docx": "file-text",
- ".rtf": "file-text",
- ".odt": "file-text",
- ".tex": "file-text",
- ".latex": "file-text",
-
- // Image files
- ".png": "file-image",
- ".jpg": "file-image",
- ".jpeg": "file-image",
- ".gif": "file-image",
- ".svg": "file-image",
- ".webp": "file-image",
- ".bmp": "file-image",
- ".ico": "file-image",
- ".tiff": "file-image",
- ".tif": "file-image",
-
- // Audio files
- ".mp3": "file-music",
- ".wav": "file-music",
- ".flac": "file-music",
- ".aac": "file-music",
- ".ogg": "file-music",
- ".m4a": "file-music",
- ".wma": "file-music",
-
- // Video files
- ".mp4": "play",
- ".avi": "play",
- ".mov": "play",
- ".wmv": "play",
- ".flv": "play",
- ".webm": "play",
- ".mkv": "play",
- ".m4v": "play",
- ".3gp": "play",
-
- // Archive files
- ".zip": "file-archive",
- ".rar": "file-archive",
- ".7z": "file-archive",
- ".tar": "file-archive",
- ".gz": "file-archive",
- ".tgz": "file-archive",
- ".bz2": "file-archive",
- ".xz": "file-archive",
- ".deb": "file-archive",
- ".rpm": "file-archive",
- ".dmg": "file-archive",
- ".pkg": "file-archive",
- ".msi": "file-archive",
- ".exe": "file-archive",
-
- // Font files
- ".ttf": "file-type",
- ".otf": "file-type",
- ".woff": "file-type",
- ".woff2": "file-type",
- ".eot": "file-type",
-
- // Other common files
- ".key": "file-cog",
- ".pem": "file-cog",
- ".crt": "file-cog",
- ".cert": "file-cog",
- ".p12": "file-cog",
- ".pfx": "file-cog",
- ".lock": "file-cog",
- ".tmp": "file",
- ".bak": "file",
- ".old": "file",
- ".orig": "file",
- ".save": "file",
-};
-
-function getFileIcon(name) {
- const ext = "." + name.split(".").pop().toLowerCase();
- return EXT_ICONS[ext] || "file";
-}
-
-// ---------------------------------------------------------------------------
-// Safe CDN helpers
-// ---------------------------------------------------------------------------
-
-let _iconDebounceTimer = null;
-
-/**
- * Debounced icon creation — batches multiple rapid calls into one
- * DOM scan to avoid excessive reflows when building large trees.
- */
-function safeCreateIcons() {
- if (typeof lucide === "undefined" || !lucide.createIcons) return;
- if (state._iconDebounceTimer) return; // already scheduled
- state._iconDebounceTimer = requestAnimationFrame(() => {
- state._iconDebounceTimer = null;
- try {
- lucide.createIcons();
- } catch (e) {
- /* CDN not loaded */
- }
- });
-}
-
-/** Force-flush icon creation immediately (use sparingly). */
-function flushIcons() {
- if (state._iconDebounceTimer) {
- cancelAnimationFrame(state._iconDebounceTimer);
- state._iconDebounceTimer = null;
- }
- if (typeof lucide !== "undefined" && lucide.createIcons) {
- try {
- lucide.createIcons();
- } catch (e) {
- /* CDN not loaded */
- }
- }
-}
-
-function safeHighlight(block) {
- // Skip mermaid blocks — they're rendered by Mermaid.js, not highlighted
- if (block.className && block.className.indexOf('language-mermaid') !== -1) return;
- if (typeof hljs !== "undefined" && hljs.highlightElement) {
- try {
- hljs.highlightElement(block);
- } catch (e) {
- /* CDN not loaded */
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-function escapeHtml(str) {
- if (!str) return "";
- return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
-}
-
-// ---------------------------------------------------------------------------
-// Editor (CodeMirror)
-// ---------------------------------------------------------------------------
-async function openEditor(vaultName, filePath) {
- state.editorVault = vaultName;
- state.editorPath = filePath;
-
- const modal = document.getElementById("editor-modal");
- const titleInput = document.getElementById("editor-title-input");
- const filenameInput = document.getElementById("editor-filename-input");
- const statFile = document.getElementById("editor-stat-file");
- const statWords = document.getElementById("editor-stat-words");
- const bodyEl = document.getElementById("editor-body");
-
- // Restore header/footer (may be hidden by Forge)
- var headerEl = modal.querySelector(".editor-header");
- var footerEl = modal.querySelector(".editor-footer");
- var container = modal.querySelector(".editor-container");
- if (headerEl) headerEl.style.display = "";
- if (footerEl) footerEl.style.display = "";
- if (container) container.style.maxHeight = "";
-
- filenameInput.value = filePath.split("/").pop();
- statFile.textContent = vaultName + "/" + filePath;
-
- // Fetch raw content
- const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
- const rawData = await api(rawUrl);
-
- // Clear previous editor
- bodyEl.innerHTML = "";
- if (state.editorView) {
- state.editorView.destroy();
- state.editorView = null;
- }
- state.fallbackEditorEl = null;
-
- // Re-create AI toolbar container (was cleared by innerHTML above)
- const aiContainer = document.createElement('div');
- aiContainer.id = 'ai-toolbar-container';
- bodyEl.appendChild(aiContainer);
-
- 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,
- // Auto-save: debounced save after 2s of inactivity
- EditorView.updateListener.of((update) => {
- if (update.docChanged) {
- clearTimeout(window._obsigateAutoSaveTimer);
- window._obsigateAutoSaveTimer = setTimeout(() => saveFile(true), 2000);
- // Mark dirty
- var sd = document.getElementById("editor-save-dot");
- var sl = document.getElementById("editor-save-label");
- if (sd) sd.className = "editor-save-dot dirty";
- if (sl) sl.textContent = "Unsaved";
- }
- }),
- ];
-
- // Add language support based on file extension
- 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, // Using javascript for shell scripts as fallback
- bash: javascript,
- zsh: javascript,
- };
-
- const langMode = langMap[fileExt];
- if (langMode) {
- extensions.push(langMode());
- }
-
- if (currentTheme === "dark") {
- extensions.push(oneDark);
- }
-
- const cmState = EditorState.create({
- doc: rawData.raw,
- extensions: extensions,
- });
-
- state.editorView = new EditorView({
- state: cmState,
- parent: bodyEl,
- });
- } catch (err) {
- console.error("CodeMirror init failed, falling back to textarea:", err);
- state.fallbackEditorEl = document.createElement("textarea");
- state.fallbackEditorEl.className = "fallback-editor";
- state.fallbackEditorEl.value = rawData.raw;
- bodyEl.appendChild(state.fallbackEditorEl);
- }
-
- // Set up AI toolbar (recreate each time editor opens)
- var container = document.getElementById('ai-toolbar-container');
- if (container && !container.querySelector('.ai-toolbar') && !container.querySelector('div')) {
- createAIToolbar(container, function() { return state.editorView || null; });
- }
-
- // Mermaid live preview panel (#26)
- var existingPreview = document.getElementById('mermaid-live-preview');
- if (existingPreview) existingPreview.remove();
- var previewPanel = document.createElement('div');
- previewPanel.id = 'mermaid-live-preview';
- previewPanel.className = 'mermaid-live-preview';
- previewPanel.style.display = 'none';
- bodyEl.appendChild(previewPanel);
-
- // Debounced live preview on content change
- var mermaidTimer = null;
- var updateMermaidPreview = function() {
- clearTimeout(mermaidTimer);
- mermaidTimer = setTimeout(function() {
- var content = state.editorView ? state.editorView.state.doc.toString() : (state.fallbackEditorEl ? state.fallbackEditorEl.value : '');
- var match = content.match(/```mermaid\n([\s\S]*?)```/);
- if (match) {
- previewPanel.style.display = 'block';
- previewPanel.innerHTML = '';
- var code = match[1];
- import('./mermaid-viewer.js').then(function(m) {
- // Create a temp container and render
- var tmp = document.createElement('div');
- tmp.innerHTML = '' + code.replace(/&/g,'&').replace(//g,'>') + ' ';
- previewPanel.appendChild(tmp);
- m.renderMermaidBlocks(previewPanel);
- });
- } else {
- previewPanel.style.display = 'none';
- }
- }, 500);
- };
-
- if (state.editorView) {
- // Attach to CodeMirror
- var extUpdate = state.editorView.state.updateListener;
- // Use the existing update listener approach - just add debounce on any change
- }
-
- modal.classList.add("active");
- safeCreateIcons();
-
- // Initial preview if content has mermaid
- updateMermaidPreview();
-}
-
-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");
- }
-}
-
-function closeEditor() {
- const vault = state.editorVault;
- const path = state.editorPath;
- const modal = document.getElementById("editor-modal");
- modal.classList.remove("active");
- // Restore brand text
- var brand = modal.querySelector('.editor-brand');
- if (brand) brand.textContent = 'ObsiGate';
- if (state.editorView) {
- state.editorView.destroy();
- state.editorView = null;
- }
- state.fallbackEditorEl = null;
- // Remove forge iframe if present + refresh current file
- var forgeFrame = document.getElementById("forge-iframe");
- if (forgeFrame) {
- forgeFrame.remove();
- if (state.currentVault && state.currentPath) openFile(state.currentVault, state.currentPath);
- }
- state.editorVault = null;
- state.editorPath = null;
- // Refresh file content if we're viewing the edited file
- if (vault && path && state.currentVault === vault && state.currentPath === path) {
- openFile(vault, path);
- }
-}
-
-async function saveFile(silent = false) {
- // If Forge is open, delegate save to the iframe
- var forgeFrame = document.getElementById("forge-iframe");
- if (forgeFrame && forgeFrame.contentWindow) {
- if (!state.editorVault || !state.editorPath) return;
- var fSaveBtn = document.getElementById("editor-save");
- var fSaveDot = document.getElementById("editor-save-dot");
- var fSaveLabel = document.getElementById("editor-save-label");
- if (fSaveDot) { fSaveDot.className = 'editor-save-dot saving'; }
- if (fSaveLabel) fSaveLabel.textContent = 'Saving...';
- if (fSaveBtn) { fSaveBtn.disabled = true; }
- forgeFrame.contentWindow.postMessage({ type: 'parent-save' }, '*');
- return;
- }
-
- if ((!state.editorView && !state.fallbackEditorEl) || !state.editorVault || !state.editorPath) return;
-
- const content = state.editorView ? state.editorView.state.doc.toString() : state.fallbackEditorEl.value;
- const saveBtn = document.getElementById("editor-save");
- const saveDot = document.getElementById("editor-save-dot");
- const saveLabel = document.getElementById("editor-save-label");
-
- try {
- // Update save dot to saving state
- if (saveDot) { saveDot.className = 'editor-save-dot saving'; }
- if (saveLabel) saveLabel.textContent = 'Saving...';
- if (saveBtn && !silent) {
- saveBtn.disabled = true;
- saveBtn.innerHTML = ' ';
- safeCreateIcons();
- }
-
- const backupParam = silent ? '&backup=false' : '';
- const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}/save?path=${encodeURIComponent(state.editorPath)}${backupParam}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ content }),
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || "Erreur de sauvegarde");
- }
-
- // Update save dot to saved state
- if (saveDot) { saveDot.className = 'editor-save-dot ok'; }
- if (saveLabel) saveLabel.textContent = 'Saved';
-
- if (silent) {
- // Auto-save: brief green flash on save button
- if (saveBtn) {
- saveBtn.style.background = '#22c55e';
- saveBtn.style.color = '#fff';
- saveBtn.style.borderColor = '#22c55e';
- }
- setTimeout(() => {
- if (saveBtn) {
- saveBtn.style.background = '';
- saveBtn.style.color = '';
- saveBtn.style.borderColor = '';
- }
- }, 1500);
- } else {
- closeEditor();
- }
- } catch (err) {
- console.error("Save error:", err);
- if (saveDot) { saveDot.className = 'editor-save-dot err'; }
- if (saveLabel) saveLabel.textContent = 'Erreur';
- // If offline, queue the save for later sync
- if (!navigator.onLine) {
- try {
- const { queueOfflineSave } = await import('./offline.js');
- await queueOfflineSave(state.editorVault, state.editorPath, content);
- alert('Vous etes hors-ligne. La modification a ete enregistree localement.');
- } catch (e) { alert('Erreur: ' + err.message); }
- } else {
- alert('Erreur: ' + err.message);
- }
- }
-}
-
-async function deleteFile() {
- // If Forge is open, delegate delete to the iframe
- var forgeFrame = document.getElementById("forge-iframe");
- if (forgeFrame && forgeFrame.contentWindow) {
- if (!state.editorVault || !state.editorPath) return;
- forgeFrame.contentWindow.postMessage({ type: 'parent-delete' }, '*');
- return;
- }
-
- if (!state.editorVault || !state.editorPath) return;
-
- const deleteBtn = document.getElementById("editor-delete");
- const originalHTML = deleteBtn.innerHTML;
- let success = false;
-
- try {
- deleteBtn.disabled = true;
- deleteBtn.innerHTML = ' ';
- safeCreateIcons();
-
- const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}?path=${encodeURIComponent(state.editorPath)}`, { method: "DELETE" });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || "Erreur de suppression");
- }
-
- success = true;
- closeEditor();
- showWelcome();
- await refreshSidebarForContext();
- await refreshTagsForContext();
- } catch (err) {
- console.error("Delete error:", err);
- alert(`Erreur: ${err.message}`);
- } finally {
- deleteBtn.innerHTML = originalHTML;
- deleteBtn.disabled = false;
- if (success) safeCreateIcons();
- }
-}
-
-function initEditor() {
- const cancelBtn = document.getElementById("editor-cancel");
- const deleteBtn = document.getElementById("editor-delete");
- const saveBtn = document.getElementById("editor-save");
- const modal = document.getElementById("editor-modal");
-
- cancelBtn.addEventListener("click", closeEditor);
- deleteBtn.addEventListener("click", deleteFile);
- saveBtn.addEventListener("click", () => saveFile());
-
- // ESC to close
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && modal.classList.contains("active")) {
- closeEditor();
- }
- });
-
- // Fix mouse wheel scrolling in editor
- modal.addEventListener(
- "wheel",
- (e) => {
- const editorBody = document.getElementById("editor-body");
- if (editorBody && editorBody.contains(e.target)) {
- // Let the editor handle the scroll
- return;
- }
- // Prevent modal from scrolling if not in editor area
- e.preventDefault();
- },
- { passive: false },
- );
-}
-
-/** Copy text to clipboard — works on both HTTP and HTTPS */
-function copyToClipboard(text) {
- // Try modern API first (HTTPS-only)
- if (navigator.clipboard && window.isSecureContext) {
- navigator.clipboard.writeText(text).catch(() => {});
- return true;
- }
- // Fallback for HTTP contexts
- const ta = document.createElement("textarea");
- ta.value = text;
- ta.style.position = "fixed";
- ta.style.left = "-9999px";
- ta.style.top = "-9999px";
- document.body.appendChild(ta);
- ta.focus();
- ta.select();
- try {
- document.execCommand("copy");
- return true;
- } catch (e) {
- return false;
- } finally {
- document.body.removeChild(ta);
- }
-}
-
-export { getFileIcon, safeCreateIcons, flushIcons, safeHighlight, escapeHtml, EXT_ICONS, openEditor, closeEditor, saveFile, deleteFile, waitForCodeMirror, initEditor, copyToClipboard };
diff --git a/desktop/frontend-static/js/vaulthome.js b/desktop/frontend-static/js/vaulthome.js
deleted file mode 100644
index f21f44b..0000000
--- a/desktop/frontend-static/js/vaulthome.js
+++ /dev/null
@@ -1,384 +0,0 @@
-/**
- * Vault Home Page module for ObsiGate.
- *
- * When a specific vault is selected (not "All"), replaces the dashboard
- * with a non-recursive file listing sorted by modification time (newest first).
- * Only files from the selected directory are shown (not subdirectories)
- * to keep the listing fast and lightweight.
- * The listing updates automatically when browsing into subdirectories.
- *
- * The visual style mirrors the search results (.search-result-item) for
- * consistency.
- *
- * Usage:
- * import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js';
- */
-
-import { api } from './auth.js';
-import { el } from './viewer.js';
-import { getFileIcon, safeCreateIcons } from './utils.js';
-import { TabManager } from './ui.js';
-import { t, getLocale } from './i18n.js';
-
-// ---------------------------------------------------------------------------
-// State
-// ---------------------------------------------------------------------------
-
-const state = {
- active: false,
- currentVault: null,
- currentDir: '', // empty = vault root
-};
-
-// ---------------------------------------------------------------------------
-// Public API
-// ---------------------------------------------------------------------------
-
-/** Check if vault home is currently displayed. */
-export function isVaultHomeActive() {
- return state.active;
-}
-
-/**
- * Display the vault home page for a vault directory.
- * @param {string} vaultName - Vault name
- * @param {string} dirPath - Directory path (empty for root)
- */
-export async function showVaultHome(vaultName, dirPath = '') {
- if (vaultName === 'all' || !vaultName) {
- state.active = false;
- return;
- }
-
- state.active = true;
- state.currentVault = vaultName;
- state.currentDir = dirPath || '';
-
- const area = document.getElementById('content-area');
- if (!area) return;
-
- // Remove any existing dashboard
- const home = document.getElementById('dashboard-home');
- if (home) home.remove();
-
- // Build vault home container
- area.innerHTML = '';
-
- const container = el('div', { class: 'vault-home', id: 'vault-home' });
- area.appendChild(container);
-
- // Breadcrumb
- renderBreadcrumb(container, vaultName, state.currentDir);
-
- // Results container
- const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' });
- container.appendChild(resultsArea);
-
- // Show loading
- resultsArea.innerHTML = `
-
-
-
${t('vaulthome.loading')}
-
`;
-
- await loadAndRender(vaultName, state.currentDir, resultsArea);
-
- // Also load recent files for this vault (only at root level)
- if (!state.currentDir) {
- loadRecentSection(vaultName, container);
- }
-}
-
-/**
- * Update the vault home directory without rebuilding the whole page.
- * Called when navigating into a subdirectory.
- * @param {string} vaultName
- * @param {string} dirPath
- */
-export async function updateVaultHomeDir(vaultName, dirPath) {
- if (!state.active || state.currentVault !== vaultName) return;
-
- state.currentDir = dirPath || '';
-
- const container = document.getElementById('vault-home');
- if (!container) return;
-
- // Update breadcrumb
- const bc = container.querySelector('.vault-home-breadcrumb');
- if (bc) {
- bc.innerHTML = '';
- buildBreadcrumbHTML(bc, vaultName, state.currentDir);
- safeCreateIcons();
- }
-
- // Remove recent section when navigating into subdirectory
- const recentSection = container.querySelector('.vault-home-recent');
- if (recentSection) recentSection.remove();
-
- // Reload results
- const resultsArea = document.getElementById('vault-home-results');
- if (resultsArea) {
- resultsArea.innerHTML = `
- `;
- await loadAndRender(vaultName, state.currentDir, resultsArea);
- }
-}
-
-// ---------------------------------------------------------------------------
-// Tree click delegation — update vault home on directory clicks
-// ---------------------------------------------------------------------------
-
-let _treeDelegationInstalled = false;
-
-function _initTreeDelegation() {
- if (_treeDelegationInstalled) return;
-
- const tree = document.getElementById('vault-tree');
- if (!tree) {
- // Retry once when DOM might not be ready — use MutationObserver as fallback
- const observer = new MutationObserver(() => {
- const t = document.getElementById('vault-tree');
- if (t) {
- observer.disconnect();
- _attachTreeListener(t);
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- return;
- }
-
- _attachTreeListener(tree);
-}
-
-function _attachTreeListener(tree) {
- if (_treeDelegationInstalled) return;
- _treeDelegationInstalled = true;
-
- tree.addEventListener('click', (e) => {
- if (!state.active) return;
-
- // Find the closest tree-item that is a directory
- const item = e.target.closest('.tree-item');
- if (!item || !item.dataset.vault || item.dataset.path === undefined) return;
-
- const vault = item.dataset.vault;
- const path = item.dataset.path;
-
- // Only react to directory clicks (items that have a child container)
- const hasChildContainer = item.nextElementSibling
- && item.nextElementSibling.classList.contains('tree-children');
- if (!hasChildContainer) return;
-
- // Update vault home dir
- updateVaultHomeDir(vault, path);
- });
-}
-
-// Initialize delegation when module loads
-_initTreeDelegation();
-
-// ---------------------------------------------------------------------------
-// Rendering
-// ---------------------------------------------------------------------------
-
-function renderBreadcrumb(container, vaultName, dirPath) {
- const bc = el('div', { class: 'vault-home-breadcrumb' });
- buildBreadcrumbHTML(bc, vaultName, dirPath);
- container.appendChild(bc);
-}
-
-function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
- // Vault icon + name (root)
- const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [
- el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }),
- el('span', {}, [document.createTextNode(vaultName)]),
- ]);
- vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, ''));
- vaultLink.style.cursor = 'pointer';
- parentEl.appendChild(vaultLink);
-
- if (!dirPath) return;
-
- const segments = dirPath.split('/').filter(Boolean);
- let cumulative = '';
- for (const seg of segments) {
- cumulative = cumulative ? cumulative + '/' + seg : seg;
- const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]);
- parentEl.appendChild(sep);
- const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [
- el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }),
- el('span', {}, [document.createTextNode(seg)]),
- ]);
- segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative));
- segLink.style.cursor = 'pointer';
- parentEl.appendChild(segLink);
- }
-}
-
-async function loadAndRender(vaultName, dirPath, container) {
- try {
- const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=false`;
- const data = await api(url);
-
- if (!data.files || data.files.length === 0) {
- container.innerHTML = `
-
-
-
${t('vaulthome.empty')}
-
`;
- safeCreateIcons();
- return;
- }
-
- // Header
- const header = el('div', { class: 'vault-home-header' });
- const countKey = data.files.length > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count';
- const countStr = t(countKey, { count: data.files.length });
- const count = el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [
- document.createTextNode(countStr),
- ]);
- header.appendChild(count);
-
- // Directory label
- const dirLabel = dirPath ? `/${dirPath}` : ` ${t('common.root')}`;
- const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
- document.createTextNode(dirLabel),
- ]);
- header.appendChild(dirInfo);
-
- container.innerHTML = '';
- container.appendChild(header);
-
- // File list — using search-result-item style
- const list = el('div', { class: 'search-results' });
- const locale = getLocale();
-
- data.files.forEach((f) => {
- const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
- const displayName = f.name.endsWith('.md') ? f.name.slice(0, -3) : f.name;
- const ext = f.extension || '';
-
- const titleDiv = el('div', { class: 'search-result-title' }, [
- document.createTextNode(displayName),
- ]);
-
- // Format date using current locale
- const modDate = new Date(f.modified * 1000);
- const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', {
- year: 'numeric', month: 'short', day: 'numeric',
- hour: '2-digit', minute: '2-digit',
- });
-
- // Build metadata line: date + size
- const metaParts = [];
- metaParts.push(dateStr);
- metaParts.push(formatSize(f.size));
-
- const metaDiv = el('div', { class: 'search-result-vault' }, [
- document.createTextNode(metaParts.join(' · ')),
- ]);
-
- const item = el('div', {
- class: 'search-result-item vault-home-item',
- 'data-vault': f.vault,
- 'data-path': f.path,
- }, [
- el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
- el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
- titleDiv,
- metaDiv,
- ]);
-
- item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
- item.addEventListener('dblclick', (e) => {
- e.preventDefault();
- TabManager.openPersistent(f.vault, f.path);
- });
-
- list.appendChild(item);
- });
-
- container.appendChild(list);
- safeCreateIcons();
- } catch (err) {
- const errMsg = t('vaulthome.error_loading') + ' ' + escapeHtml(err?.detail || err?.message || t('common.unknown'));
- container.innerHTML = `
- `;
- }
-}
-
-// ---------------------------------------------------------------------------
-// Recent files section (loaded at vault root only)
-// ---------------------------------------------------------------------------
-
-async function loadRecentSection(vaultName, container) {
- try {
- const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`);
- const files = data.files || [];
- if (files.length === 0) return;
-
- // Insert recent section BEFORE the results area
- const resultsArea = document.getElementById('vault-home-results');
- const section = el('div', { class: 'vault-home-recent' });
-
- const header = el('div', { class: 'vault-home-recent-header' });
- header.innerHTML = `
-
- ${t('dashboard.recent')} `;
- section.appendChild(header);
-
- const list = el('div', { class: 'vault-home-recent-list' });
- files.forEach((f) => {
- const title = f.title || f.path.split('/').pop() || '';
- const displayTitle = title.endsWith('.md') ? title.slice(0, -3) : title;
- const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : '';
-
- const item = el('div', { class: 'vault-home-recent-item' });
- item.innerHTML = `
-
- ${escapeHtml(displayTitle)}
- ${escapeHtml(dir)}
- ${escapeHtml(f.mtime_human || '')} `;
-
- item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
- item.addEventListener('dblclick', (e) => {
- e.preventDefault();
- TabManager.openPersistent(f.vault, f.path);
- });
- list.appendChild(item);
- });
-
- section.appendChild(list);
-
- // Insert before the results area
- if (resultsArea) {
- container.insertBefore(section, resultsArea);
- } else {
- container.appendChild(section);
- }
-
- safeCreateIcons();
- } catch {
- // Silently skip if recent files can't be loaded
- }
-}
-
-// ---------------------------------------------------------------------------
-// Utility
-// ---------------------------------------------------------------------------
-
-function formatSize(bytes) {
- if (bytes < 1024) return bytes + ' ' + t('vaulthome.size_bytes');
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' ' + t('vaulthome.size_kb');
- return (bytes / (1024 * 1024)).toFixed(1) + ' ' + t('vaulthome.size_mb');
-}
-
-function escapeHtml(str) {
- const div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
-}
diff --git a/desktop/frontend-static/js/viewer.js b/desktop/frontend-static/js/viewer.js
deleted file mode 100644
index 735f04f..0000000
--- a/desktop/frontend-static/js/viewer.js
+++ /dev/null
@@ -1,1550 +0,0 @@
-/* ObsiGate — Viewer module */
-import { api } from './auth.js';
-import { state } from './state.js';
-import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon, openEditor, copyToClipboard } from './utils.js';
-import { TabManager, closeMobileSidebar, ContextMenuManager, RightSidebarManager, showToast, buildFrontmatterCard } from './ui.js';
-import { syncActiveFileTreeItem, searchByTag, TagFilterService, refreshSidebarTreePreservingState, focusPathInSidebar } from './sidebar.js';
-import { AutocompleteDropdown, performAdvancedSearch } from './search.js';
-import { initDashboardTabs } from './sync.js';
-import { renderMermaidBlocks } from './mermaid-viewer.js';
-import { DashboardStatsWidget, DashboardRecentWidget, DashboardBookmarkWidget, DashboardSharedWidget, DashboardConflictsWidget } from './dashboard.js';
-import { openShareDialog } from './config.js';
-import { cacheViewedFile, getCachedFile } from './offline.js';
-import { t } from './i18n.js';
-// ---------------------------------------------------------------------------
-// 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);
- });
-
- state.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 (state.activeHeadingId === headingId) return;
-
- state.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();
- state.headingsCache = [];
- state.activeHeadingId = null;
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Scroll Spy Manager
-// ---------------------------------------------------------------------------
-
-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 = [];
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// Reading Progress Manager
-// ---------------------------------------------------------------------------
-
-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%";
- },
-};
-
-
-// ---------------------------------------------------------------------------
-// File viewer
-// ---------------------------------------------------------------------------
-export async function openFile(vaultName, filePath) {
- state.currentVault = vaultName;
- state.currentPath = filePath;
- state.showingSource = false;
- state.cachedRawSource = null;
-
- // Highlight active
- syncActiveFileTreeItem(vaultName, filePath);
-
- // Show loading state while fetching
- const area = document.getElementById("content-area");
- area.innerHTML = '';
-
- try {
- const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
- const data = await api(url);
- // Cache the file for offline use
- if (data && !data.unsupported && data.content) {
- cacheViewedFile(vaultName, filePath, data.content, data.modified);
- }
- renderFile(data);
- } catch (err) {
- // Try IndexedDB fallback
- console.log('[Viewer] Network failed, trying offline cache:', err);
- const cached = await getCachedFile(vaultName, filePath);
- if (cached && cached.content) {
- const fakeData = {
- vault: vaultName,
- path: filePath,
- title: filePath.split('/').pop().replace(/\.md$/, ''),
- content: cached.content,
- html: null,
- raw_size: cached.content.length,
- modified: cached.modified || '',
- tags: [],
- offline: true
- };
- renderFile(fakeData);
- showToast(t('viewer.loaded_from_cache'), 'info');
- } else {
- area.innerHTML = 'Impossible de charger le fichier — vous etes hors-ligne et ce fichier n\'est pas en cache.
';
- }
- }
-}
-
-async function renderBacklinksPanel(vault, path, container) {
- try {
- const data = await api(`/api/file/${encodeURIComponent(vault)}/backlinks?path=${encodeURIComponent(path)}`);
- if (!data.backlinks || data.backlinks.length === 0) return;
-
- const panel = el("div", { class: "backlinks-panel" });
- const header = el("div", { class: "backlinks-header" }, [
- icon("link", 14),
- document.createTextNode(` ${data.total} lien(s) entrant(s)`),
- ]);
- panel.appendChild(header);
-
- const list = el("div", { class: "backlinks-list" });
- data.backlinks.forEach((bl) => {
- const item = el("div", { class: "backlink-item" });
- const vaultBadge = el("span", { class: "backlink-vault" }, [document.createTextNode(bl.vault)]);
- const titleEl = el("span", { class: "backlink-title" }, [document.createTextNode(bl.title || bl.path.split("/").pop().replace(/\.md$/i, ""))]);
- item.appendChild(icon(getFileIcon(bl.path), 12));
- item.appendChild(vaultBadge);
- item.appendChild(titleEl);
- item.addEventListener("click", () => TabManager.openPreview(bl.vault, bl.path));
- item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(bl.vault, bl.path); });
- list.appendChild(item);
- });
- panel.appendChild(list);
- container.appendChild(panel);
- } catch (err) {
- // Silently ignore — backlinks are optional
- console.debug("Backlinks fetch failed:", err);
- }
-}
-
-export function renderFile(data) {
- const area = document.getElementById("content-area");
-
- // Handle PDF files — render in iframe
- if (data.is_pdf) {
- const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf/stream?path=${encodeURIComponent(data.path)}`;
- const pages = data.pdf_metadata && data.pdf_metadata.pages ? data.pdf_metadata.pages : '?';
- area.innerHTML = `
-
-
- PDF — ${pages} pages
-
- Plein écran
-
-
- Télécharger
-
-
-
-
`;
- lucide.createIcons();
- return;
- }
-
- // Handle unsupported (binary) files
- if (data.unsupported) {
- const sizeStr = data.size_bytes
- ? data.size_bytes < 1024 ? `${data.size_bytes} o`
- : data.size_bytes < 1048576 ? `${(data.size_bytes / 1024).toFixed(1)} Ko`
- : `${(data.size_bytes / 1048576).toFixed(1)} Mo`
- : "";
- area.innerHTML = `
-
-
-
${escapeHtml(data.path.split("/").pop())}
-
Ce fichier est binaire et ne peut pas être affiché.
- ${sizeStr ? `
Taille : ${sizeStr}
` : ""}
-
- Télécharger
-
-
`;
- lucide.createIcons();
- document.getElementById("unsupported-download-btn").addEventListener("click", () => {
- const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`;
- window.open(dlUrl, "_blank");
- });
- return;
- }
-
- // Breadcrumb
- const parts = data.path.split("/");
- const breadcrumbEls = [];
- breadcrumbEls.push(
- makeBreadcrumbSpan(data.vault, () => {
- focusPathInSidebar(data.vault, "", { alignToTop: "center" });
- }),
- );
- 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, () => {
- focusPathInSidebar(data.vault, p, { alignToTop: "center" });
- }),
- );
- } else {
- breadcrumbEls.push(
- makeBreadcrumbSpan(part.replace(/\.md$/i, ""), () => {
- focusPathInSidebar(data.vault, data.path, { alignToTop: "center" });
- }),
- );
- }
- });
-
- const breadcrumb = el("div", { class: "breadcrumb" }, breadcrumbEls);
-
- // Tags
- const tagsDiv = el("div", { class: "file-tags" });
- (data.tags || []).forEach((tag) => {
- if (!TagFilterService.isTagFiltered(tag)) {
- const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
- t.addEventListener("click", () => searchByTag(tag));
- tagsDiv.appendChild(t);
- }
- });
-
- // Action buttons
- const copyBtn = el("button", { class: "btn-action", title: "Copier la source" }, [icon("copy", 14), document.createTextNode("Copier")]);
- copyBtn.addEventListener("click", async () => {
- try {
- // Fetch raw content if not already cached
- if (!state.cachedRawSource) {
- const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
- const rawData = await api(rawUrl);
- state.cachedRawSource = rawData.raw;
- }
- if (navigator.clipboard) {
- await navigator.clipboard.writeText(state.cachedRawSource);
- } else {
- // Fallback for non-secure contexts
- const ta = document.createElement("textarea");
- ta.value = state.cachedRawSource;
- ta.style.position = "fixed"; ta.style.opacity = "0";
- document.body.appendChild(ta); ta.select();
- document.execCommand("copy");
- document.body.removeChild(ta);
- }
- copyBtn.lastChild.textContent = t('viewer.copied');
- setTimeout(() => (copyBtn.lastChild.textContent = t('viewer.copy')), 1500);
- } catch (err) {
- console.error("Copy error:", err);
- showToast(t("viewer.copy_error"), "error");
- }
- });
-
- const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [icon("code", 14), document.createTextNode("Source")]);
-
- // MD download button
- const mdBtn = el("button", { class: "btn-action", title: t("viewer.download_md") }, [icon("file-text", 14), document.createTextNode(".md")]);
- mdBtn.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);
- });
-
- // PDF download button
- const pdfBtn = el("button", { class: "btn-action", title: t("viewer.download_pdf") }, [icon("file", 14), document.createTextNode("PDF")]);
- pdfBtn.addEventListener("click", () => {
- const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf?path=${encodeURIComponent(data.path)}`;
- window.open(pdfUrl, "_blank");
- });
-
- const editBtn = el("button", { class: "btn-action", title: "Editer" }, [icon("edit", 14), document.createTextNode("Editer")]);
- editBtn.addEventListener("click", () => {
- openEditor(data.vault, data.path);
- });
-
- const forgeBtn = el("button", { class: "btn-action btn-forge", title: t("viewer.forge_title") }, [icon("hammer", 14), document.createTextNode("Forge")]);
- forgeBtn.addEventListener("click", () => {
- var modal = document.getElementById("editor-modal");
- var container = modal.querySelector(".editor-container");
- var bodyEl = document.getElementById("editor-body");
- // Set editor state so parent buttons work
- state.editorVault = data.vault;
- state.editorPath = data.path;
- // Update header: show "Forge" brand + filename
- var brand = modal.querySelector(".editor-brand");
- var titleInp = document.getElementById("editor-title-input");
- var fileInp = document.getElementById("editor-filename-input");
- if (brand) brand.textContent = t("viewer.forge_brand");
- if (titleInp) { titleInp.value = data.title || ''; titleInp.placeholder = t('forge.title_placeholder'); }
- if (fileInp) fileInp.value = data.path.split('/').pop();
- bodyEl.innerHTML = "";
- bodyEl.style.overflow = "hidden";
- var iframe = document.createElement("iframe");
- iframe.id = "forge-iframe";
- iframe.src = "/editor-poc?vault=" + encodeURIComponent(data.vault) + "&path=" + encodeURIComponent(data.path) + "&_ts=" + Date.now();
- iframe.style.cssText = "width:100%;height:82vh;border:none;display:block;";
- bodyEl.appendChild(iframe);
- modal.classList.add("active");
- safeCreateIcons();
- });
-
- const openNewWindowBtn = el("button", { class: "btn-action", title: t("forge.popout") }, [icon("external-link", 14), document.createTextNode(t("forge.popout_label"))]);
- openNewWindowBtn.addEventListener("click", () => {
- const theme = document.documentElement.getAttribute('data-theme') || 'dark';
- const popoutUrl = `/popout/${encodeURIComponent(data.vault)}/${encodeURIComponent(data.path)}?theme=${theme}`;
- window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, "_")}`, "width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no");
- });
-
- const tocBtn = el("button", { class: "btn-action", id: "toc-toggle-btn", title: "Afficher/Masquer le sommaire" }, [icon("list", 14), document.createTextNode("TOC")]);
- tocBtn.addEventListener("click", () => {
- RightSidebarManager.toggle();
- });
-
- // Share button — check if already shared
- const shareBtn = el("button", { class: "btn-action btn-share", title: "Partager ce document" }, [icon("share-2", 14), document.createTextNode("Partager")]);
- // Check if already shared and color the button
- (async () => {
- try {
- const shares = await api("/api/shares");
- if (shares.some(s => s.vault === data.vault && s.path === data.path)) {
- shareBtn.classList.add("shared");
- shareBtn.title = t('viewer.shared_doc_title');
- }
- } catch (e) { /* ignore */ }
- })();
- shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
-
- // Bookmark button — check if already bookmarked
- const bookmarkBtn = el("button", { class: "btn-action btn-bookmark", title: t("viewer.bookmark_toggle") }, [icon("bookmark-plus", 14), document.createTextNode("Bookmark")]);
- // Check bookmark status and color the button
- (async () => {
- try {
- const bms = await api("/api/bookmarks");
- if (Array.isArray(bms) && bms.some(b => b.vault === data.vault && b.path === data.path)) {
- bookmarkBtn.classList.add("active");
- bookmarkBtn.title = t("viewer.bookmark_remove");
- }
- } catch (e) { /* ignore */ }
- })();
- bookmarkBtn.addEventListener("click", async () => {
- try {
- const res = await api("/api/bookmarks/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vault: data.vault, path: data.path, title: data.title }) });
- bookmarkBtn.classList.toggle("active", res.bookmarked);
- bookmarkBtn.title = res.bookmarked ? t("viewer.bookmark_remove") : t("viewer.bookmark_add");
- showToast(res.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");
- if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load();
- } catch (err) { showToast("Erreur: " + err.message, "error"); }
- });
-
- // Frontmatter — Accent Card
- let fmSection = null;
- if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
- fmSection = buildFrontmatterCard(data.frontmatter);
- }
-
- // Content container (rendered HTML)
- const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" });
- mdDiv.innerHTML = data.html;
-
- // Raw source container (hidden initially)
- const rawDiv = el("div", { class: "raw-source-view", id: "file-raw-content", style: "display:none" });
-
- // Source button toggle logic
- sourceBtn.addEventListener("click", async () => {
- const rendered = document.getElementById("file-rendered-content");
- const raw = document.getElementById("file-raw-content");
- if (!rendered || !raw) return;
-
- state.showingSource = !state.showingSource;
- if (state.showingSource) {
- sourceBtn.classList.add("active");
- if (!state.cachedRawSource) {
- const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
- const rawData = await api(rawUrl);
- state.cachedRawSource = rawData.raw;
- }
- raw.textContent = state.cachedRawSource;
- rendered.style.display = "none";
- raw.style.display = "block";
- } else {
- sourceBtn.classList.remove("active");
- rendered.style.display = "block";
- raw.style.display = "none";
- }
- });
-
- // Assemble
- area.innerHTML = "";
- area.appendChild(breadcrumb);
- area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn, sourceBtn, mdBtn, pdfBtn, editBtn, forgeBtn, openNewWindowBtn, tocBtn, shareBtn, bookmarkBtn])]));
- if (fmSection) area.appendChild(fmSection);
- area.appendChild(mdDiv);
- area.appendChild(rawDiv);
-
- // Backlinks panel
- if (data.is_markdown) {
- renderBacklinksPanel(data.vault, data.path, area);
- }
-
- // Highlight code blocks
- area.querySelectorAll("pre code").forEach((block) => {
- safeHighlight(block);
- });
-
- // Add copy buttons to code blocks
- area.querySelectorAll("pre").forEach((pre) => {
- if (pre.parentElement?.classList.contains("code-block-wrapper")) return;
- const wrapper = el("div", { class: "code-block-wrapper" }, []);
- wrapper.style.position = "relative";
- pre.replaceWith(wrapper);
- wrapper.appendChild(pre);
-
- const btn = el("button", { class: "copy-code-btn", title: "Copier le code" }, [document.createTextNode("Copier")]);
- btn.addEventListener("click", (e) => {
- e.stopPropagation();
- const code = pre.querySelector("code")?.textContent || pre.textContent;
- const copied = copyToClipboard(code);
- if (copied) {
- btn.textContent = "✓ Copié";
- btn.classList.add("copied");
- if (typeof showToast === "function") showToast("Code copié dans le presse-papiers", "success");
- setTimeout(() => { btn.textContent = "Copier"; btn.classList.remove("copied"); }, 2000);
- } else {
- if (typeof showToast === "function") showToast("Erreur lors de la copie", "error");
- }
- });
- wrapper.appendChild(btn);
- });
-
- // Render Mermaid diagrams
- renderMermaidBlocks(area);
-
- // 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) TabManager.openPreview(v, p);
- });
- });
-
- safeCreateIcons();
- area.scrollTop = 0;
-
- // Initialize outline/TOC for this document
- OutlineManager.init();
-}
-
-
-// ---------------------------------------------------------------------------
-// Helpers (escapeHtml imported from utils.js)
-// ---------------------------------------------------------------------------
-export function el(tag, attrs, children) {
- const e = document.createElement(tag);
- if (attrs) {
- Object.entries(attrs).forEach(([k, v]) => {
- // Skip boolean false for standard HTML boolean attributes to avoid setAttribute("checked", "false") bug
- if (v === false && (k === "checked" || k === "disabled" || k === "hidden" || k === "required" || k === "readonly")) {
- return;
- }
- e.setAttribute(k, v);
- });
- }
- if (children) {
- children.forEach((c) => {
- if (c) e.appendChild(c);
- });
- }
- return e;
-}
-
-export 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;
-}
-
-export function smallBadge(count) {
- const s = document.createElement("span");
- s.className = "badge-small";
- s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px";
- s.textContent = `(${count})`;
- return s;
-}
-
-function getContextMenuPositionFromElement(target) {
- const rect = target.getBoundingClientRect();
- return {
- x: Math.min(rect.right - 8, window.innerWidth - 16),
- y: Math.min(rect.top + rect.height / 2, window.innerHeight - 16),
- };
-}
-
-export function attachTreeItemActionButton(itemEl, vault, path, type, isReadonly) {
- const button = document.createElement("button");
- button.type = "button";
- button.className = "tree-item-action-btn";
- button.setAttribute("aria-label", "Afficher le menu d’actions");
- button.setAttribute("title", "Actions");
- const iconEl = icon("more-vertical", 16);
- button.appendChild(iconEl);
- button.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- const pos = getContextMenuPositionFromElement(button);
- ContextMenuManager.show(pos.x, pos.y, vault, path, type, isReadonly);
- });
- itemEl.appendChild(button);
- // Ensure Lucide icons are rendered for the button
- setTimeout(() => {
- safeCreateIcons();
- }, 0);
-}
-
-export function attachTreeItemLongPress(itemEl, getMenuData) {
- let pressTimer = null;
- let pressHandled = false;
- let startX = 0;
- let startY = 0;
- const longPressDelay = 550;
- const moveThreshold = 10;
-
- const clearPressTimer = () => {
- if (pressTimer) {
- clearTimeout(pressTimer);
- pressTimer = null;
- }
- };
-
- itemEl.addEventListener("touchstart", (e) => {
- if (!e.touches || e.touches.length !== 1) return;
- pressHandled = false;
- startX = e.touches[0].clientX;
- startY = e.touches[0].clientY;
- clearPressTimer();
- pressTimer = setTimeout(() => {
- const data = getMenuData();
- if (!data) return;
- pressHandled = true;
- ContextMenuManager.show(startX, startY, data.vault, data.path, data.type, data.isReadonly);
- }, longPressDelay);
- }, { passive: true });
-
- itemEl.addEventListener("touchmove", (e) => {
- if (!e.touches || e.touches.length !== 1) return;
- const dx = Math.abs(e.touches[0].clientX - startX);
- const dy = Math.abs(e.touches[0].clientY - startY);
- if (dx > moveThreshold || dy > moveThreshold) {
- clearPressTimer();
- }
- }, { passive: true });
-
- itemEl.addEventListener("touchend", () => {
- clearPressTimer();
- }, { passive: true });
-
- itemEl.addEventListener("touchcancel", () => {
- clearPressTimer();
- }, { passive: true });
-
- itemEl.addEventListener("click", (e) => {
- if (pressHandled) {
- e.preventDefault();
- e.stopPropagation();
- setTimeout(() => {
- pressHandled = false;
- }, 0);
- }
- }, true);
-}
-
-export function getVaultIcon(vaultName, size = 16) {
- const v = state.allVaults.find((val) => val.name === vaultName);
- const type = v ? v.type : "VAULT";
-
- if (type === "DIR") {
- const i = icon("folder", size);
- i.style.color = "#eab308"; // yellow tint
- return i;
- } else {
- const purple = "#8b5cf6";
- const svgNS = "http://www.w3.org/2000/svg";
- const svg = document.createElementNS(svgNS, "svg");
- svg.setAttribute("xmlns", svgNS);
- svg.setAttribute("width", size);
- svg.setAttribute("height", size);
- svg.setAttribute("viewBox", "0 0 24 24");
- svg.setAttribute("fill", "none");
- svg.setAttribute("stroke", purple);
- svg.setAttribute("stroke-width", "2");
- svg.setAttribute("stroke-linecap", "round");
- svg.setAttribute("stroke-linejoin", "round");
- svg.classList.add("icon");
-
- const path1 = document.createElementNS(svgNS, "path");
- path1.setAttribute("d", "M6 3h12l4 6-10 12L2 9z");
- const path2 = document.createElementNS(svgNS, "path");
- path2.setAttribute("d", "M11 3 8 9l4 12");
- const path3 = document.createElementNS(svgNS, "path");
- path3.setAttribute("d", "M12 21l4-12-3-6");
- const path4 = document.createElementNS(svgNS, "path");
- path4.setAttribute("d", "M2 9h20");
-
- svg.appendChild(path1);
- svg.appendChild(path2);
- svg.appendChild(path3);
- svg.appendChild(path4);
- return svg;
- }
-}
-
-function makeBreadcrumbSpan(text, onClick) {
- const s = document.createElement("span");
- s.textContent = text;
- if (onClick) {
- s.addEventListener("click", async (event) => {
- event.preventDefault();
- if (s.dataset.busy === "true") return;
- s.dataset.busy = "true";
- s.style.pointerEvents = "none";
- try {
- await onClick(event);
- } finally {
- s.dataset.busy = "false";
- s.style.pointerEvents = "";
- }
- });
- }
- return s;
-}
-
-export function appendHighlightedText(container, text, query, caseSensitive) {
- container.textContent = "";
- if (!query) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- const source = caseSensitive ? text : text.toLowerCase();
- const needle = caseSensitive ? query : query.toLowerCase();
- let start = 0;
- let index = source.indexOf(needle, start);
-
- if (index === -1) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- while (index !== -1) {
- if (index > start) {
- container.appendChild(document.createTextNode(text.slice(start, index)));
- }
- const mark = el("mark", { class: "filter-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]);
- container.appendChild(mark);
- start = index + query.length;
- index = source.indexOf(needle, start);
- }
-
- if (start < text.length) {
- container.appendChild(document.createTextNode(text.slice(start)));
- }
-}
-
-export function highlightSearchText(container, text, query, caseSensitive) {
- container.textContent = "";
- if (!query || !text) {
- container.appendChild(document.createTextNode(text || ""));
- return;
- }
-
- const source = caseSensitive ? text : text.toLowerCase();
- const needle = caseSensitive ? query : query.toLowerCase();
- let start = 0;
- let index = source.indexOf(needle, start);
-
- if (index === -1) {
- container.appendChild(document.createTextNode(text));
- return;
- }
-
- while (index !== -1) {
- if (index > start) {
- container.appendChild(document.createTextNode(text.slice(start, index)));
- }
- const mark = el("mark", { class: "search-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]);
- container.appendChild(mark);
- start = index + query.length;
- index = source.indexOf(needle, start);
- }
-
- if (start < text.length) {
- container.appendChild(document.createTextNode(text.slice(start)));
- }
-}
-
-export function showWelcome() {
- hideProgressBar();
-
- // Restore or rebuild the dashboard with tabbed sections
- const area = document.getElementById("content-area");
- const home = document.getElementById("dashboard-home");
-
- if (area && !home) {
- area.innerHTML = `
-
-
-
-
- ${t('dashboard.stats')}
-
-
- ${t('dashboard.bookmarks')}
-
-
- ${t('dashboard.tab_recent')}
-
-
- ${t('dashboard.tab_shared')}
-
-
-
-
-
-
-
${t('common.loading')}
-
-
-
-
-
-
-
-
-
-
${t('dashboard.no_bookmarks')}
-
${t('dashboard.pin_hint')}
-
-
-
-
-
-
-
-
-
-
-
${t('dashboard.no_recent')}
-
${t('dashboard.recent_hint')}
-
-
-
-
-
-
-
-
-
${t('dashboard.no_shares')}
-
${t('dashboard.shared_hint')}
-
-
-
`;
-
- // Re-initialize widgets and dashboard tabs
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.init();
- }
- initDashboardTabs();
- safeCreateIcons();
- } else if (home) {
- // Dashboard already exists, show it with default tab
- home.style.display = "";
- // Reset tabs to default
- document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
- document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
- const defaultTab = document.querySelector('.dashboard-tab[data-tab="stats"]');
- const defaultPanel = document.getElementById("dashboard-panel-stats");
- if (defaultTab) defaultTab.classList.add("active");
- if (defaultPanel) defaultPanel.classList.add("active");
- }
-
- // Load all widgets (they handle missing elements gracefully)
- if (typeof DashboardStatsWidget !== "undefined") {
- DashboardStatsWidget.load();
- }
- if (typeof DashboardConflictsWidget !== "undefined") {
- DashboardConflictsWidget.load();
- }
- if (typeof DashboardRecentWidget !== "undefined") {
- DashboardRecentWidget.load(state.selectedContextVault);
- }
- if (typeof DashboardBookmarkWidget !== "undefined") {
- DashboardBookmarkWidget.load(state.selectedContextVault);
- }
- if (typeof DashboardSharedWidget !== "undefined") {
- DashboardSharedWidget.load();
- }
-
- // Load saved searches sidebar
- loadSavedSearches();
-}
-
-export async function loadSavedSearches() {
- const list = document.getElementById("saved-searches-list");
- const empty = document.getElementById("saved-searches-empty");
- if (!list) return;
- try {
- const searches = await api("/api/saved-searches");
- if (!searches.length) {
- list.innerHTML = "";
- if (empty) empty.style.display = "";
- return;
- }
- if (empty) empty.style.display = "none";
- list.innerHTML = searches.map(s => {
- // Detect directory-based saved search (empty query + include_paths)
- var isDirSearch = (!s.query || s.query.trim() === '') && s.include_paths;
- var displayQuery = isDirSearch ? (s.include_paths || 'Dossier') : escapeHtml(s.query);
- // Icon: folder-search for directories, search for regular queries
- var typeIcon = isDirSearch
- ? ' '
- : ' ';
- const badges = [];
- if (s.case_sensitive) badges.push('Aa ');
- if (s.whole_word) badges.push('wd ');
- if (s.regex) badges.push('.* ');
- const pathFilters = [];
- if (s.include_paths) pathFilters.push(`📥 ${escapeHtml(s.include_paths)} `);
- if (s.exclude_paths) pathFilters.push(`📤 ${escapeHtml(s.exclude_paths)} `);
- const vaultStr = s.vault && s.vault !== "all" ? `📁 ${escapeHtml(s.vault)} ` : "";
- return `
-
-
${displayQuery}
-
- ${badges.join("")} ${vaultStr}
- ${typeIcon}
-
- ${pathFilters.length ? '
' + pathFilters.join(" ") + '
' : ""}
-
✕
-
- `}).join("");
- // Apply saved filter
- _applySavedFilter();
- // Set up filter bar (once)
- _initSavedFilter();
- list.querySelectorAll(".saved-search-item").forEach(item => {
- item.addEventListener("click", (e) => {
- if (e.target.classList.contains("saved-search-delete")) return;
- const idx = Array.from(list.children).indexOf(item);
- const s = searches[idx];
- if (!s) return;
- // Apply the saved search
- const input = document.getElementById("search-input");
- if (input) input.value = s.query;
- state.searchCaseSensitive = s.case_sensitive || false;
- state.searchWholeWord = s.whole_word || false;
- state.searchRegex = s.regex || false;
- if (typeof _updateToggleUI === "function") _updateToggleUI();
- if (s.include_paths) {
- const incl = document.getElementById("search-include-input");
- if (incl) incl.value = s.include_paths;
- }
- if (s.exclude_paths) {
- const excl = document.getElementById("search-exclude-input");
- if (excl) excl.value = s.exclude_paths;
- }
- // Execute the search — suppress dropdown from appearing
- AutocompleteDropdown.hide();
- AutocompleteDropdown._suppressNext = true;
- const vault = s.vault || "all";
- if (input) { input.dispatchEvent(new Event("input")); }
- clearTimeout(state.searchTimeout);
- state.advancedSearchOffset = 0;
- performAdvancedSearch(s.query, vault, null);
- });
- });
- list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
- e.stopPropagation();
- await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
- loadSavedSearches();
- }));
- safeCreateIcons();
- } catch (err) { /* silently ignore */ }
-}
-
-export function showLoading() {
- const area = document.getElementById("content-area");
- area.innerHTML = `
-
-
-
Recherche en cours...
-
`;
- showProgressBar();
-}
-
-export function showProgressBar() {
- const bar = document.getElementById("search-progress-bar");
- if (bar) bar.classList.add("active");
-}
-
-export function hideProgressBar() {
- const bar = document.getElementById("search-progress-bar");
- if (bar) bar.classList.remove("active");
-}
-
-function goHome() {
- const searchInput = document.getElementById("search-input");
- if (searchInput) searchInput.value = "";
-
- document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
-
- state.currentVault = null;
- state.currentPath = null;
- state.showingSource = false;
- state.cachedRawSource = null;
-
- closeMobileSidebar();
- showWelcome();
-}
-
-
-// ---------------------------------------------------------------------------
-// SSE Client — IndexUpdateManager
-// ---------------------------------------------------------------------------
-const IndexUpdateManager = (() => {
- let eventSource = null;
- let reconnectTimer = null;
- let reconnectDelay = 1000;
- const MAX_RECONNECT_DELAY = 30000;
- let recentEvents = [];
- const MAX_RECENT_EVENTS = 20;
- let connectionState = "disconnected"; // disconnected | connecting | connected
-
- function connect() {
- if (eventSource) {
- eventSource.close();
- }
- connectionState = "connecting";
- _updateBadge();
-
- eventSource = new EventSource("/api/events");
-
- eventSource.addEventListener("connected", (e) => {
- connectionState = "connected";
- reconnectDelay = 1000;
- _updateBadge();
- });
-
- eventSource.addEventListener("index_updated", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_updated", data);
- _onIndexUpdated(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_reloaded", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_reloaded", data);
- _onIndexReloaded(data);
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_added", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_added", data);
- showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("vault_removed", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("vault_removed", data);
- showToast(`Vault "${data.vault}" supprimé`, "info");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_start", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_start", data);
- connectionState = "syncing";
- _updateBadge();
- showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_progress", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_progress", data);
- connectionState = "syncing";
- _updateBadge();
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.addEventListener("index_complete", (e) => {
- try {
- const data = JSON.parse(e.data);
- _addEvent("index_complete", data);
- connectionState = "connected";
- _updateBadge();
- showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
- loadVaults();
- loadTags();
- } catch (err) {
- console.error("SSE parse error:", err);
- }
- });
-
- eventSource.onerror = () => {
- connectionState = "disconnected";
- _updateBadge();
- eventSource.close();
- eventSource = null;
- _scheduleReconnect();
- };
- }
-
- function _scheduleReconnect() {
- if (reconnectTimer) clearTimeout(reconnectTimer);
- reconnectTimer = setTimeout(() => {
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
- connect();
- }, reconnectDelay);
- }
-
- function _addEvent(type, data) {
- recentEvents.unshift({
- type,
- data,
- timestamp: new Date().toISOString(),
- });
- if (recentEvents.length > MAX_RECENT_EVENTS) {
- recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
- }
- }
-
- async function _onIndexUpdated(data) {
- // Brief syncing state
- connectionState = "syncing";
- _updateBadge();
-
- const n = data.total_changes || 0;
- const vaults = (data.vaults || []).join(", ");
- // Toast removed: silent auto-indexing — no notification needed
-
- // Refresh sidebar and tags if affected vault matches current context
- const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
- if (affectsCurrentVault) {
- try {
- await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
- // Refresh current file if it was updated
- if (state.currentVault && state.currentPath) {
- const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath);
- if (changed) {
- openFile(state.currentVault, state.currentPath);
- }
- }
- } catch (err) {
- console.error("Error refreshing after index update:", err);
- }
- }
-
- // Refresh recent tab if it is active
- if (state.activeSidebarTab === "recent") {
- const vaultFilter = document.getElementById("recent-vault-filter");
- loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
- }
-
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- async function _onIndexReloaded(data) {
- connectionState = "syncing";
- _updateBadge();
- showToast(t("sync.index_reloaded"), "info");
- try {
- await Promise.all([loadVaults(), loadTags()]);
- } catch (err) {
- console.error("Error refreshing after full reload:", err);
- }
- setTimeout(() => {
- connectionState = "connected";
- _updateBadge();
- }, 1500);
- }
-
- function _updateBadge() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
- badge.className = "sync-badge sync-badge--" + connectionState;
- const labels = {
- disconnected: "Déconnecté",
- connecting: "Connexion...",
- connected: "Synchronisé",
- syncing: t("viewer.syncing"),
- };
- badge.title = labels[connectionState] || connectionState;
- }
-
- function disconnect() {
- if (eventSource) {
- eventSource.close();
- eventSource = null;
- }
- if (reconnectTimer) {
- clearTimeout(reconnectTimer);
- reconnectTimer = null;
- }
- connectionState = "disconnected";
- _updateBadge();
- }
-
- function getState() {
- return connectionState;
- }
-
- function getRecentEvents() {
- return recentEvents;
- }
-
- return { connect, disconnect, getState, getRecentEvents };
-})();
-
-function initSyncStatus() {
- const badge = document.getElementById("sync-badge");
- if (!badge) return;
-
- badge.addEventListener("click", (e) => {
- e.stopPropagation();
- toggleSyncPanel();
- });
-
- IndexUpdateManager.connect();
-}
-
-function toggleSyncPanel() {
- let panel = document.getElementById("sync-panel");
- if (panel) {
- panel.remove();
- return;
- }
- // Auto reconnect if disconnected when user opens the panel
- if (IndexUpdateManager.getState() === "disconnected") {
- IndexUpdateManager.connect();
- }
- panel = document.createElement("div");
- panel.id = "sync-panel";
- panel.className = "sync-panel";
- _renderSyncPanel(panel);
- document.body.appendChild(panel);
-
- // Close on outside click
- setTimeout(() => {
- document.addEventListener("click", _closeSyncPanelOutside, { once: true });
- }, 0);
-}
-
-function _closeSyncPanelOutside(e) {
- const panel = document.getElementById("sync-panel");
- if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
- panel.remove();
- }
-}
-
-function _renderSyncPanel(panel) {
- const state = IndexUpdateManager.getState();
- const events = IndexUpdateManager.getRecentEvents();
-
- const stateLabels = {
- disconnected: "Déconnecté",
- connecting: "Connexion...",
- connected: "Connecté",
- syncing: "Synchronisation...",
- };
-
- let html = ``;
-
- if (events.length === 0) {
- html += `Aucun événement récent
`;
- } else {
- html += ``;
- events.slice(0, 10).forEach((ev) => {
- const time = new Date(ev.timestamp).toLocaleTimeString();
- const typeLabels = {
- index_updated: t("viewer.index_updated"),
- index_reloaded: "Rechargement",
- vault_added: t("sync.vault_added_label"),
- vault_removed: t("sync.vault_removed_label"),
- index_start: t("viewer.index_start"),
- index_progress: t("sync.vault_indexed_label"),
- index_complete: "Indexation tech.",
- };
- const label = typeLabels[ev.type] || ev.type;
- let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
- if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
- if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
- if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
- html += `
- ${label}
- ${detail}
- ${time}
-
`;
- });
- html += `
`;
- }
-}
-
-// ── Saved searches filter ──
-var _savedFilterType = 'all';
-var _savedFilterInitDone = false;
-
-function _initSavedFilter() {
- if (_savedFilterInitDone) return;
- var bar = document.getElementById('saved-filter-bar');
- if (!bar) return;
- bar.querySelectorAll('.saved-filter-pill').forEach(function(btn) {
- btn.addEventListener('click', function() {
- bar.querySelectorAll('.saved-filter-pill').forEach(function(b) { b.classList.remove('active'); });
- btn.classList.add('active');
- _savedFilterType = btn.dataset.filter;
- _applySavedFilter();
- });
- });
- _savedFilterInitDone = true;
-}
-
-function _applySavedFilter() {
- var items = document.querySelectorAll('.saved-search-item');
- var hasVisible = false;
- items.forEach(function(item) {
- var type = item.dataset.type;
- var show = _savedFilterType === 'all' || type === _savedFilterType;
- item.style.display = show ? '' : 'none';
- if (show) hasVisible = true;
- });
- // Show/hide empty state
- var empty = document.getElementById('saved-searches-empty');
- if (empty) empty.style.display = hasVisible ? 'none' : '';
-}
-
-
diff --git a/desktop/frontend-static/locales/en.json b/desktop/frontend-static/locales/en.json
deleted file mode 100644
index 5e5fbeb..0000000
--- a/desktop/frontend-static/locales/en.json
+++ /dev/null
@@ -1,1499 +0,0 @@
-{
- "_meta": {
- "locale": "en",
- "name": "English"
- },
- "about.api_version": "API version",
- "about.backend": "Backend",
- "about.browser": "Browser",
- "about.built_with": "Built with",
- "about.editor": "Editor",
- "about.file_watcher": "File watching",
- "about.files": "Files",
- "about.files_indexed": "Indexed files",
- "about.footer": "Made with ☕, 🍕 and a few accidental git push --force.",
- "about.frontend": "Frontend",
- "about.fun_quote": "\"No LLM was harmed during this development.\"",
- "about.git": "Git : — ",
- "about.icons": "Icons",
- "about.joke_0": "0 known bugs. The others are shy.",
- "about.joke_1": "Compiles on the first try. (Except Monday mornings.)",
- "about.joke_2": "Written by a human. Probably.",
- "about.joke_3": "No LLMs were harmed during this development.",
- "about.joke_4": "When it works, we call it a feature.",
- "about.joke_5": "CSS: 90% of dev time, 10% of the code.",
- "about.joke_6": "Dark mode active since the project's birth.",
- "about.joke_7": "Works on my machine™",
- "about.joke_8": "Technically, everything is an edge case.",
- "about.joke_9": "The CSS for this panel took longer than the backend.",
- "about.language": "Language",
- "about.last_delivery": "Last delivery : June 2026 ",
- "about.made_with": "Made with ☕, 🎧 and a few accidental git push --force.",
- "about.markdown_renderer": "Markdown rendering",
- "about.name": "Name",
- "about.platform": "Platform",
- "about.section_app": "Application",
- "about.section_components": "Components",
- "about.section_env": "Environment",
- "about.status": "Status",
- "about.subtitle": "Your Obsidian notes, simply accessible.",
- "about.syntax_highlight": "Syntax highlighting",
- "about.tagline": "Your Obsidian notes, simply accessible.",
- "about.tests": "Tests",
- "about.vaults_configured": "Configured vaults",
- "about.version": "Version",
- "admin.close": "Close",
- "admin.col_actions": "Actions",
- "admin.col_last_login": "Last login",
- "admin.col_role": "Role",
- "admin.col_status": "Status",
- "admin.col_user": "User",
- "admin.col_vaults": "Vaults",
- "admin.new_user": "+ New user",
- "admin.title": "Administration — Users",
- "ai.casual": "Casual tone",
- "ai.completion_added": "AI: completion added",
- "ai.continue": "Continue",
- "ai.custom_rewrite": "Custom rewrite",
- "ai.done": "AI: text processed",
- "ai.edit": "Edit",
- "ai.error_invalid_key": "Invalid API key",
- "ai.explain": "Explain",
- "ai.fix_spelling": "Fix spelling",
- "ai.frontmatter": "Generate frontmatter",
- "ai.generate": "Generate",
- "ai.hint_add_details": "Adds details",
- "ai.hint_edit_selection": "Modifies the selected text",
- "ai.hint_generate": "Generates content from the selection",
- "ai.hint_improve": "Improves text quality",
- "ai.hint_translate": "Translates the selected text",
- "ai.improve": "Improve",
- "ai.inline_completion_title": "AI Inline Completion (Ctrl+J) — Auto-complete text",
- "ai.invalid_api_key": "AI: invalid API key. Check DEEPSEEK_API_KEY in .env",
- "ai.longer": "Longer",
- "ai.no_api_key": "No API key configured",
- "ai.not_configured": "⚠️ AI not configured — add DEEPSEEK_API_KEY, OPENROUTER_API_KEY or GEMINI_API_KEY in .env",
- "ai.processing": "⏳ AI: processing...",
- "ai.professional": "Professional tone",
- "ai.quota_exceeded": "AI: quota exceeded or payment required",
- "ai.rewrite": "💬 Rewrite",
- "ai.rewrite_done": "AI: text rewritten",
- "ai.rewrite_prompt": "Rewrite instruction:",
- "ai.rewrite_title": "Rewrite text according to your instructions",
- "ai.select_text": "AI: select text to process",
- "ai.shorter": "Shorter",
- "ai.simplify": "Simplify",
- "ai.summarize": "Summarize",
- "ai.text_processed": "AI: text processed ✓",
- "ai.text_rewritten": "AI: text rewritten",
- "ai.to_canvas": "Convert to Canvas",
- "ai.to_list": "Convert to list",
- "ai.to_table": "Convert to table",
- "ai.toolbar_edit": "Edit",
- "ai.toolbar_generate": "Generate",
- "ai.toolbar_rewrite": "Rewrite",
- "ai.toolbar_tone": "Tone",
- "ai.toolbar_toolbox": "Toolbox",
- "ai.toolbar_translate": "Translate",
- "ai.toolbox": "🧰 Toolbox",
- "ai.translate_to": "Translate to {lang}",
- "auth.cannot_delete_self": "Cannot delete your own account",
- "auth.logged_out": "Signed out",
- "auth.login_button": "Sign in",
- "auth.login_error": "Invalid credentials",
- "auth.login_title": "Secure access to your vaults",
- "auth.logout_title": "Sign out",
- "auth.network_error": "Network error — check your connection",
- "auth.new_password": "New password (empty = unchanged)",
- "auth.password": "Password",
- "auth.password_min": "Min. 8 characters",
- "auth.remember_me": "Remember me (30 days)",
- "auth.session_expired": "Session expired, please sign in again",
- "auth.user_deleted": "User deleted",
- "auth.username": "Username",
- "auto.0257e77f": "🧰 Toolbox ▾",
- "auto.033a9ddf": "Default model",
- "auto.03a6e5b1": "The operators used are displayed as",
- "auto.13c3ee2c": "Search and autocomplete",
- "auto.13eb7a51": "AI: invalid API key",
- "auto.14c75ac6": "Select desired events",
- "auto.195a8e44": "Create directory",
- "auto.1a54bd69": "Combined example",
- "auto.1a8a8118": "Drag and drop",
- "auto.1abf6661": "The path displayed above the content is",
- "auto.1d3330cc": "No automatic retry on failure",
- "auto.1d46301c": "Line numbers",
- "auto.1efbbc9a": "Editing with CodeMirror 6",
- "auto.209c5490": "The top bar contains the essential elements:",
- "auto.272dd29a": "Supported providers",
- "auto.27a2f7c1": "ObsiGate supports opening multiple files simultaneously in tabs.",
- "auto.28bf6aab": "Security: HMAC-SHA256 signature",
- "auto.2a5af19b": "The workflow triggers automatically",
- "auto.2bc24d2e": "All sensitive operations are logged",
- "auto.2bc66bab": "Directory creation via API",
- "auto.337d8626": "Light/dark theme",
- "auto.37561472": "🛡️ Security",
- "auto.396d0dd8": "The AI toolbar appears below the title",
- "auto.3be698f6": "Favor operators",
- "auto.404c8f1c": "Autocomplete helps you quickly find",
- "auto.41d7c6af": "Dedicated directory",
- "auto.460b8298": "Data",
- "auto.4bc1f00a": "Use operators to refine your",
- "auto.4c908ade": "Click a vault to open it and see its",
- "auto.4d281c6c": "AI: text processed ✓",
- "auto.4dfa6a25": "No vault filtering — a webhook listens to",
- "auto.4fa5d7c2": "Quick editing",
- "auto.559ef1da": "Recommended workflow",
- "auto.598a0ca7": "Tracked events",
- "auto.5a486420": "Delete directory",
- "auto.5d4a2516": "Open a file in Edit mode (✏️ button",
- "auto.5d936af6": "Generated regex",
- "auto.62c3d50d": "💬 Rewrite",
- "auto.678711b5": "Select a context",
- "auto.695d9e47": "CodeMirror 6 Editor",
- "auto.6c166d35": "ObsiGate is your web gateway to your Obsidian vaults. Access, browse, and search all your notes from any device via a modern and responsive interface.",
- "auto.72e515fa": "Access control by vault",
- "auto.753d30a8": "Open a file to see it",
- "auto.774dbd63": "The Command Palette allows quick keyboard access to all ObsiGate functions. Use it to open files, execute commands, and navigate without touching the mouse.",
- "auto.79f6e1dd": "Multi-window",
- "auto.7c9cdc3c": "Check the events to listen to",
- "auto.7e13c97a": "These settings apply immediately",
- "auto.83a0efdf": "Reindexing",
- "auto.9122fd5c": "Advanced configuration",
- "auto.98c547ea": "do not trigger",
- "auto.a05514cb": "Creation via API or palette",
- "auto.a105a2d9": "Edit ▾",
- "auto.a131dfab": "CodeMirror Editor",
- "auto.a232672b": "Deployment",
- "auto.a3319169": "📦 About",
- "auto.aa8ca265": "The view uses the Barnes-Hut algorithm to calculate the layout in O(n log n), ensuring smooth performance even with hundreds of nodes.",
- "auto.ab5e6155": "ObsiGate automatically creates backups before",
- "auto.aedff644": "Create markdown file",
- "auto.b114ed1c": "These settings are saved on the server. Some require a restart or reindexing.",
- "auto.b419c2c2": "Python receiver (Flask)",
- "auto.b86ba5ea": "Multi-criteria search",
- "auto.b893650a": "Edit current file",
- "auto.ba768f01": "Supported events",
- "auto.c089d968": "Pin files to find them",
- "auto.c14b5603": "🎨 Themes",
- "auto.c4922a99": "🤖 Artificial Intelligence in the Editor",
- "auto.caac40b9": "Choose a visual theme. Each theme offers",
- "auto.cd7332c8": "Results include facets to refine",
- "auto.ce4eb802": "Generate ▾",
- "auto.d33601dc": "Real-time file monitoring via",
- "auto.d4d28717": "Trigger",
- "auto.e0b8a606": "Define tag patterns to hide from",
- "auto.e61598c1": "Share a document to see it",
- "auto.ec7260fa": "Supported file types",
- "auto.edf89626": "The built-in editor offers:",
- "auto.ef8c5f4d": "The built-in editor offers a complete editing experience:",
- "auto.f346076a": "✏️ Advanced Editing",
- "auto.f5a8c116": "Event",
- "auto.f8ba6127": "🗂️ Hidden files",
- "auto.fc694490": "Deletion via API or editor",
- "backup.delete": "Delete",
- "backup.diff": "Compare",
- "backup.file_restored": "File restored successfully",
- "backup.no_backups": "No backups",
- "backup.preview": "Preview",
- "backup.purge": "Purge",
- "backup.restore": "Restore",
- "backup.restore_failed": "Restore failed",
- "backup.select_to_restore": "Select a backup to restore",
- "backup.timestamp_not_found": "Cannot find backup timestamp",
- "backup.title": "Backups",
- "backup.version": "Version from {date}",
- "backups.delete_confirm": "Delete this backup?",
- "backups.empty": "No backups available for this file.",
- "backups.hint": "Backups are created automatically before each modification.",
- "backups.loading": "Loading backups...",
- "backups.purge_file": "Delete ALL backups of",
- "backups.purge_vault": "Delete ALL backups from vault",
- "backups.restore_confirm": "to the selected backup version?\nThe current version will be saved.",
- "backups.title": "Backups",
- "button.cancel": "Cancel",
- "common.actions": "Actions",
- "common.all": "All",
- "common.back": "Back",
- "common.cancel": "Cancel",
- "common.close": "Close",
- "common.confirm": "Confirm",
- "common.copied": "Copied!",
- "common.copy": "Copy",
- "common.create": "Create",
- "common.delete": "Delete",
- "common.download": "Download",
- "common.edit": "Edit",
- "common.error": "Error",
- "common.loading": "Loading...",
- "common.move": "Move",
- "common.never": "Never",
- "common.next": "Next",
- "common.no": "No",
- "common.none": "None",
- "common.ok": "OK",
- "common.previous": "Previous",
- "common.refresh": "Refresh",
- "common.rename": "Rename",
- "common.root": "(root)",
- "common.save": "Save",
- "common.search": "Search",
- "common.unknown": "Unknown",
- "common.yes": "Yes",
- "config.about": "About",
- "config.about_error": "Loading error",
- "config.about_loading": "Loading...",
- "config.add_webhook": "Add",
- "config.ai_deepseek_label": "DeepSeek API Key",
- "config.ai_default_provider": "Default provider",
- "config.ai_gemini_label": "Gemini API Key",
- "config.ai_keys": "AI API keys",
- "config.ai_keys_desc": "Configure API keys for the AI editor.",
- "config.ai_model": "Model",
- "config.ai_openrouter_label": "OpenRouter API Key",
- "config.api_keys_saved": "API keys saved",
- "config.backups": "Backups",
- "config.backups_desc": "Manage automatic file backups.",
- "config.client_config": "Client config",
- "config.create_share": "Create share",
- "config.debounce_label": "Debounce delay (ms)",
- "config.diagnostics": "Diagnostics",
- "config.diagnostics_desc": "Technical information about the server state.",
- "config.display_name": "Display name",
- "config.error_prefix": "Error",
- "config.error_unknown": "Unknown error",
- "config.force_reindex": "Force reindex",
- "config.hidden_files": "Hidden files",
- "config.hidden_files_desc": "Hide files/folders starting with a dot in the interface (they remain indexed and searchable)",
- "config.hidden_files_toggle": "Hide hidden files/folders",
- "config.hidden_include_label": "Include hidden files",
- "config.hidden_whitelist_label": "Whitelist (comma-separated)",
- "config.hint_activer_d_sactiver_la_surveillance_automatique_des": "Enable/disable automatic file monitoring",
- "config.hint_annuler_la_recherche_apr_s_ce_d_lai_5000_120000_": "Cancel search after this delay (5000-120000)",
- "config.hint_contenu_index_par_fichier_10k_1m_r_indexation_requ": "Max content size per file (bytes)",
- "config.hint_d_lai_avant_ex_cution_de_la_recherche_100_2000_": "Delay before search execution (100-2000)",
- "config.hint_d_lai_avant_traitement_des_changements_group_s_0_5": "Delay before processing grouped changes (0.5-10 seconds)",
- "config.hint_entre_5_et_100_fichiers_sauvegarde_sur_le_serveur_": "Between 5 and 100 files (stored on server)",
- "config.hint_forcer_le_mode_polling_au_lieu_de_inotify_natif_ut": "Forcer le mode polling au lieu de inotify natif (utile si le mode natif ne fonctionne pas)",
- "config.hint_intervalle_de_scrutation_en_mode_polling_1_30_seco": "Intervalle de scrutation en mode polling (1-30 secondes)",
- "config.hint_multiplicateur_de_pertinence_pour_les_correspondan": "Multiplicateur de pertinence pour les correspondances dans le titre",
- "config.hint_nombre_de_r_sultats_affich_s_par_page_10_200_": "Number of results displayed per page (10-200)",
- "config.hint_nombre_max_de_tokens_largis_par_pr_fixe_10_200_": "Max expanded tokens per prefix (10-200)",
- "config.hint_nombre_maximum_de_versions_backup_conservees_par_f": "Maximum backup versions kept per file (1-50)",
- "config.hint_nombre_minimum_de_caract_res_avant_recherche_1_5_": "Minimum characters before search triggers (1-5)",
- "config.hint_threads_d_di_s_la_recherche_1_8_": "Dedicated search threads (1-8)",
- "config.just_now": "just now",
- "config.lang_hint": "Used by autocomplete and ghost text.",
- "config.lang_label": "Preferred language",
- "config.link_copied": "Link copied!",
- "config.link_created": "Link created!",
- "config.logout": "Sign out",
- "config.max_backups": "Max backups per file",
- "config.max_backups_desc": "Maximum backup versions kept per file (1-50)",
- "config.max_backups_label": "Max backups per file",
- "config.max_content_hint": "Indexed content per file (10K-1M). Reindex required.",
- "config.max_content_label": "Max content size (bytes)",
- "config.max_content_size": "Max content size (bytes)",
- "config.max_prefix_expansions": "Max prefix expansions",
- "config.min_query_label": "Min. query length",
- "config.min_query_length": "Min query length",
- "config.name_placeholder": "Your name",
- "config.no_keys": "No keys to save",
- "config.no_restart": "No restart needed",
- "config.note": "Note:",
- "config.open_new_window": "Open in new window",
- "config.prefix_expansions_hint": "Max tokens expanded by prefix (10-200)",
- "config.prefix_expansions_label": "Max prefix expansions",
- "config.profile": "User profile",
- "config.profile_desc": "Personal preferences to customize your ObsiGate experience.",
- "config.project_repo": "project repository",
- "config.recent_history_count": "Files in history",
- "config.recent_history_count_desc": "Between 5 and 100 files (saved on server)",
- "config.reindex": "Reindex",
- "config.reindex_desc": "Rebuild the search index for all vaults.",
- "config.reindex_error": "Reindex error",
- "config.reindexing": "Reindexing...",
- "config.reset": "Reset",
- "config.restart_required": "Restart required",
- "config.results_per_page": "Results per page",
- "config.results_per_page_label": "Results per page",
- "config.save": "Save",
- "config.save_ai_keys": "Save keys",
- "config.save_backend": "Save server configuration",
- "config.save_error": "Save error",
- "config.save_hidden": "Save",
- "config.saved": "✓ Saved",
- "config.saving": "Saving",
- "config.search_timeout": "Search timeout (ms)",
- "config.search_workers": "Search workers",
- "config.search_workers_hint": "Dedicated search threads (1-8)",
- "config.search_workers_label": "Search workers",
- "config.section_a-propos": "📦 About",
- "config.section_about": "About",
- "config.section_actions-sur-fichiers": "File actions",
- "config.section_activation": "Activation",
- "config.section_affichage": "Affichage",
- "config.section_ai": "AI API keys",
- "config.section_arborescence-des-vaults": "Arborescence des vaults",
- "config.section_astuces-et-bonnes-pratiques": "💡 Astuces et bonnes pratiques",
- "config.section_audit-log": "Audit log",
- "config.section_authentification": "Authentification",
- "config.section_autocompletion": "Autocompletion",
- "config.section_backend": "🖥️ Backend Settings",
- "config.section_backup-automatique": "Backup automatique",
- "config.section_backups": "Backups",
- "config.section_barre-d-outils-ai": "Barre d'outils AI",
- "config.section_breadcrumb-et-repositionnement": "Breadcrumb et repositionnement",
- "config.section_cas-d-usage": "Cas d'usage",
- "config.section_cas-d-usage-avances": "Advanced use cases",
- "config.section_chips-de-filtres-actifs": "Chips de filtres actifs",
- "config.section_cles-api-intelligence-artificielle": "🤖 Cles API Intelligence Artificielle",
- "config.section_configuration": "Configuration",
- "config.section_configuration-avancee": "Advanced configuration",
- "config.section_construit-avec": "Construit avec",
- "config.section_diag": "Diagnostics",
- "config.section_diagnostics": "Diagnostics",
- "config.section_editeur-codemirror": "CodeMirror Editor",
- "config.section_editeur-codemirror-6": "CodeMirror 6 Editor",
- "config.section_edition-avancee": "✏️ Advanced Editing",
- "config.section_edition-avec-codemirror-6": "Editing with CodeMirror 6",
- "config.section_en-tete": "Header",
- "config.section_evenements-supportes": "Supported events",
- "config.section_exemples-concrets": "Exemples concrets",
- "config.section_facettes-et-tri": "Facettes et tri",
- "config.section_fichiers": "📄 Files",
- "config.section_filtrage-de-la-sidebar": "Filtrage de la sidebar",
- "config.section_filtrage-de-tags": "Filtrage de tags",
- "config.section_filtrage-de-tags-template": "Filtrage de tags template",
- "config.section_fonctionnalites": "Features",
- "config.section_format-du-payload": "Format du payload",
- "config.section_gestion-des-onglets": "📑 Gestion des onglets",
- "config.section_hidden": "Hidden files",
- "config.section_historique-recent-redemarrage-non-requis": "📋 Recent History\n No restart required",
- "config.section_indicateurs-visuels": "Indicateurs visuels",
- "config.section_intelligence-artificielle-dans-l-editeur": "🤖 AI in the Editor",
- "config.section_interaction": "Interaction",
- "config.section_interface": "Interface",
- "config.section_interface-utilisateur": "🧭 Interface utilisateur",
- "config.section_limitations-actuelles": "Limitations actuelles",
- "config.section_mode-commandes": "Mode commandes",
- "config.section_mode-fichiers": "File mode",
- "config.section_navigation": "Navigation",
- "config.section_navigation-entre-onglets": "Navigation entre onglets",
- "config.section_onglets": "Onglets",
- "config.section_operateurs-avances": "Advanced operators",
- "config.section_optimisations": "Optimisations",
- "config.section_ouverture": "Ouverture",
- "config.section_ouverture-d-onglets": "Ouverture d'onglets",
- "config.section_palette-de-commandes": "⌨️ Palette de commandes",
- "config.section_parametres-backend-redemarrage-requis": "Backend settings\n Restart required",
- "config.section_parametres-de-recherche": "Search settings",
- "config.section_partage-et-webhooks": "🔗 Partage et Webhooks",
- "config.section_partages-publics": "📤 Partages publics",
- "config.section_perf": "⚡ Performance",
- "config.section_performance": "Performance",
- "config.section_personnalisation": "🎨 Personnalisation",
- "config.section_profil-utilisateur": "👤 Profil utilisateur",
- "config.section_profile": "User profile",
- "config.section_protection": "Protection",
- "config.section_providers-supportes": "Supported providers",
- "config.section_publication-publique": "Publication publique",
- "config.section_raccourcis-clavier": "⌨️ Raccourcis clavier",
- "config.section_recent": "📜 Recent History",
- "config.section_recherche": "🔍 Recherche",
- "config.section_recherche-et-autocompletion": "Search and autocompletion",
- "config.section_recherche-simple": "Recherche simple",
- "config.section_redimensionnement-de-la-sidebar": "Redimensionnement de la sidebar",
- "config.section_sauvegarde-automatique": "Sauvegarde automatique",
- "config.section_sauvegardes-et-audits": "💾 Sauvegardes et Audits",
- "config.section_search": "Search settings",
- "config.section_securite": "🛡️ Security",
- "config.section_securite-signature-hmac-sha256": "Security: HMAC-SHA256 signature",
- "config.section_selection-de-vault": "Vault selection",
- "config.section_server": "Server",
- "config.section_shares": "📤 Public Shares",
- "config.section_sidebar-barre-laterale": "Sidebar",
- "config.section_synchronisation-automatique": "Synchronisation automatique",
- "config.section_tag-cloud": "Tag cloud",
- "config.section_tags": "🏷️ Tag Filtering",
- "config.section_tags-inline-vs-frontmatter": "Tags inline vs frontmatter",
- "config.section_theme-clair-sombre": "Light/Dark theme",
- "config.section_themes": "🎨 Themes",
- "config.section_trucs-et-astuces": "Trucs et astuces",
- "config.section_types-de-fichiers-supportes": "Supported file types",
- "config.section_visualisation": "Visualisation",
- "config.section_vue-graphe": "🗺️ Vue Graphe",
- "config.section_watcher": "Auto sync",
- "config.section_webhooks": "🔗 Webhooks",
- "config.section_workflow-recommande": "Recommended workflow",
- "config.section_zone-de-contenu": "Zone de contenu",
- "config.server_config": "Server config",
- "config.settings_saved": "✓ Settings saved",
- "config.share_revoked": "Share revoked",
- "config.shares": "Public shares",
- "config.shares_desc": "Manage public share links for your documents.",
- "config.shares_empty": "No active shares",
- "config.tag_boost_hint": "Relevance multiplier for tag matches",
- "config.tag_boost_label": "Tag boost",
- "config.tag_filtering_label": "Tag filtering",
- "config.test": "Test",
- "config.timeout_label": "Search timeout (ms)",
- "config.title": "Settings",
- "config.title_boost": "Title boost",
- "config.title_boost_hint": "Relevance multiplier for title matches",
- "config.title_boost_label": "Title boost",
- "config.url_required": "URL required",
- "config.watcher_debounce_label": "Debounce (s)",
- "config.watcher_enabled_label": "Enable watcher",
- "config.watcher_interval_label": "Polling interval (s)",
- "config.watcher_polling_label": "Polling mode (fallback)",
- "config.webhook_name_placeholder": "Name",
- "config.webhook_url_placeholder": "URL",
- "config.webhooks": "Webhooks",
- "config.webhooks_desc": "HTTP notifications to external services on file changes.",
- "config.workers_label": "Search workers",
- "confirm.delete_dir": "Delete this folder and all its contents?",
- "confirm.delete_dir_named": "Delete \"{path}\" and ALL its contents?",
- "confirm.delete_file": "Permanently delete this file?",
- "confirm.delete_file_named": "Delete file \"{path}\"?",
- "confirm.logout": "Are you sure you want to sign out?",
- "conflict.no_conflicts": "No conflicts detected",
- "conflict.resolve_conflict": "Keep conflict version",
- "conflict.resolve_local": "Keep local version",
- "conflict.resolved": "Conflict resolved",
- "dashboard.all_vaults": "All vaults",
- "dashboard.bookmark_add": "Add to bookmarks",
- "dashboard.bookmark_remove": "Remove from bookmarks",
- "dashboard.bookmarks": "Bookmarks",
- "dashboard.conflict_date": "Conflict of ",
- "dashboard.conflict_resolved": "Conflict resolved",
- "dashboard.conflicts": "Conflicts",
- "dashboard.conflicts_title": "Sync conflicts",
- "dashboard.copy_btn": "Copy",
- "dashboard.count_files": "file",
- "dashboard.count_files_plural": "files",
- "dashboard.ctrl_hover_tip": "Hover over a document with Ctrl for a quick preview. Click Forge in the toolbar for the advanced editor.",
- "dashboard.day_ago": "{n} d ago",
- "dashboard.error_prefix": "Error: ",
- "dashboard.expires": "Expires {date}",
- "dashboard.hide": "Hide",
- "dashboard.hour_ago": "{n} h ago",
- "dashboard.just_now": "just now",
- "dashboard.keep_conflict": "Keep conflict",
- "dashboard.keep_original": "Keep original",
- "dashboard.load_error": "Loading error",
- "dashboard.loading": "Loading...",
- "dashboard.min_ago": "{n} min ago",
- "dashboard.no_bookmarks": "No bookmarks",
- "dashboard.no_conflicts": "No conflicts",
- "dashboard.no_recent": "No recent files",
- "dashboard.no_shares": "No shared documents",
- "dashboard.open_btn": "Open",
- "dashboard.pin_error": "Pin error",
- "dashboard.pin_hint": "Pin files to find them here.",
- "dashboard.quick_help": "Quick Help",
- "dashboard.recent": "Recent",
- "dashboard.recent_hint": "Open a file to see it appear here",
- "dashboard.shared_hint": "Share a document to see it appear here",
- "dashboard.shares": "Shares",
- "dashboard.show": "Show",
- "dashboard.size_bytes": " B",
- "dashboard.size_gb": " GB",
- "dashboard.size_kb": " KB",
- "dashboard.size_mb": " MB",
- "dashboard.stats": "Statistics",
- "dashboard.stats_files": "Files",
- "dashboard.stats_size": "Total size",
- "dashboard.stats_tags": "Unique tags",
- "dashboard.stats_vaults": "Vaults",
- "dashboard.tab_recent": "Recent",
- "dashboard.tab_shared": "Shared",
- "dashboard.tab_stats": "Statistics",
- "dashboard.total_files": "Total files",
- "dashboard.total_size": "Total size",
- "dashboard.total_tags": "Unique tags",
- "dashboard.vaults_count": "Vaults",
- "dashboard.views": "{count} view(s)",
- "dashboard.welcome": "Welcome to ObsiGate",
- "dashboard.zero_files": "0 files",
- "dragdrop.cross_vault_unsupported": "Cross-vault moves are not supported",
- "dragdrop.move_failed": "Move failed",
- "editor.cancel": "Cancel",
- "editor.close": "Close",
- "editor.delete_error": "Delete error",
- "editor.edit": "Edit current file",
- "editor.edit_current_desc": "Open current file in editor",
- "editor.find": "Find in file...",
- "editor.find_case": "Case sensitive",
- "editor.find_regex": "Regex",
- "editor.find_whole": "Whole word",
- "editor.forge_close_editor": "Close editor / modal",
- "editor.forge_help": "Forge editor help",
- "editor.no_results": "No results",
- "editor.replace": "Replace",
- "editor.save": "Save",
- "editor.save_error": "Save error",
- "editor.saved": "Saved ✓",
- "editor.unsaved": "Unsaved changes",
- "error.initialization": "Initialization error",
- "file.copy_path": "Copy path",
- "file.delete": "Delete",
- "file.graph_view": "Graph view",
- "file.new_file": "New file",
- "file.new_folder": "New folder",
- "file.properties": "Properties",
- "file.rename": "Rename",
- "forge.popout": "Open in new window",
- "forge.popout_label": "pop-out",
- "forge.title_placeholder": "Title...",
- "graph.back": "Back",
- "graph.btn_directory": "📁 Directory",
- "graph.btn_full": "🌐 All",
- "graph.cache_label": "(cached)",
- "graph.category": "📂 Category",
- "graph.click_to_open": "click to open",
- "graph.ctrl_hover_preview": "Ctrl+hover = full preview",
- "graph.ctrl_hover_preview_short": "Ctrl+hover = preview",
- "graph.depth": "Depth",
- "graph.depth_label": "depth={depth}",
- "graph.edges_count": "{count} links",
- "graph.error_loading": "Loading error",
- "graph.export_png": "Export PNG",
- "graph.favorites": "Favorites",
- "graph.filter_tag": "Filter by tag",
- "graph.filter_type": "Filter by type",
- "graph.focus_mode": "Focus mode",
- "graph.forward": "Forward",
- "graph.fullscreen": "Full screen",
- "graph.loading": "Building graph...",
- "graph.nodes_count": "{count} nodes",
- "graph.parent": "Parent",
- "graph.published": "Published",
- "graph.published_badge": "✅ published",
- "graph.scope_directory": "Directory",
- "graph.scope_full": "Full vault",
- "graph.title": "Graph View — {vault}{path}",
- "graph.type_directory": "Directory",
- "graph.type_file": "File",
- "graph.type_markdown": "Markdown",
- "header.admin_panel": "Administration",
- "header.config": "Settings",
- "header.help": "Help",
- "header.install_app": "Install app",
- "header.logo": "ObsiGate",
- "header.logout": "Sign out",
- "header.menu_about": "About",
- "header.menu_about_desc": "Version, credits & info",
- "header.menu_admin": "Administration",
- "header.menu_admin_desc": "Manage users",
- "header.menu_config": "Settings",
- "header.menu_config_desc": "Application settings",
- "header.menu_help": "User guide",
- "header.menu_help_desc": "Open help",
- "header.menu_logout": "Sign out",
- "header.menu_logout_desc": "End session",
- "header.menu_profile": "Profile",
- "header.menu_profile_desc": "Language & preferences",
- "header.menu_theme": "Theme",
- "header.menu_theme_light": "Light",
- "header.menu_toggle": "Menu",
- "header.profile": "Profile",
- "header.quick_help": "Quick Help",
- "header.refresh": "Refresh",
- "header.search_placeholder": "Search all vaults...",
- "header.sidebar_toggle": "Toggle sidebar",
- "header.sync_status": "Sync",
- "help.00b9572b": "Results ranked by TF-IDF relevance",
- "help.04df1506": "Global search with autocomplete",
- "help.065f53ee": "Silent auto-save after 2s of inactivity",
- "help.085ec31f": "Quick select at the top of the Vault tab",
- "help.096367af": "Visual indicators",
- "help.170a6982": "Horizontal display with the file name",
- "help.229fd3e2": "Discord notification",
- "help.2507242f": "🏷️ Tags",
- "help.29afa01d": "🪝 Webhooks",
- "help.2a2e5b0f": "⌨️ Keyboard shortcuts",
- "help.2a92203f": "Tips and tricks",
- "help.2c03a7ba": "Inline vs frontmatter tags",
- "help.2dad2bfd": "Automatic save",
- "help.381abec7": "Current limitations",
- "help.3cf370b1": "Type a file name to find it quickly. The search uses the /api/suggest API for instant suggestions. Press Enter to open the selected file.",
- "help.40dd7e38": "Test with curl",
- "help.4de7e318": "The 50 most recent searches (localStorage)",
- "help.550b0240": "Concrete examples",
- "help.5ab8c78d": "Stay open until manually closed",
- "help.5d0fbb81": "Workers, TF-IDF boosts, prefix expansions",
- "help.5f87ea79": "Tag cloud",
- "help.61ee5a22": "Interaction",
- "help.62308f96": "Ctrl+Shift+Space : File palette (search by title)",
- "help.62549eca": "💾 Backups and Audits",
- "help.63f024d4": "Template tag filtering",
- "help.686f8313": "💡 Tips and best practices",
- "help.6a5ddef2": "Tags adapt to the selected vault",
- "help.6ae5342c": "Index stats, memory, search engine",
- "help.6b88a375": "Debounce delay, results per page, timeout",
- "help.6b8fc728": "↑/↓ to navigate, Enter to select",
- "help.6d547397": "Tab : Toggle between file mode and command mode",
- "help.6e940dac": "Click the sidebar icon in the header",
- "help.733f0559": "🗺️ Navigation",
- "help.77f4f06e": "Payload format",
- "help.7bcbb2e3": "Sidebar resizing",
- "help.7db70e27": "Copy, Source, Download, Edit, Open in new window",
- "help.83f1d9ca": "Protection",
- "help.86e3825d": "🎨 Customization",
- "help.8717768b": "Node metadata (tags, links, path)",
- "help.89bd3c4c": "Only shows nodes with a specific tag",
- "help.8c5341e0": "📑 Tab management",
- "help.8c65b2f2": "🔗 Sharing and Webhooks",
- "help.8caed987": "Display",
- "help.9c7a0a8d": "🗺️ Graph View",
- "help.9edeaa11": "Controls exploration depth (0-3)",
- "help.9f78b3ec": "Easier navigation in long files",
- "help.a4a4e21b": "\"×\" button to reset the filter",
- "help.a579cf4d": "Preview note content without navigating",
- "help.a648c49e": "Facets and sorting",
- "help.ab2c4150": "Files (squares) and folders (circles) with icons",
- "help.about": "About",
- "help.access": "Access",
- "help.action_buttons": "Action buttons",
- "help.actions_available": "Buttons available above the content:",
- "help.actions_detail": "Buttons available above the content:",
- "help.activation": "Activation",
- "help.active_filter_chips": "Active filter chips",
- "help.active_tab": "Active tab",
- "help.add_remove": "Add/Remove",
- "help.adf30d78": "🔍 Search",
- "help.advanced_operators": "Advanced operators",
- "help.advanced_use_cases": "Advanced use cases",
- "help.ai_completes": "Completes",
- "help.ai_features": "Edit, tone, translate, generate and tools via AI.",
- "help.ai_flash_green": "flashes green",
- "help.ai_tone_casual": "Casual tone",
- "help.all_vaults": "All vaults",
- "help.atomic_writes": "Atomic writes",
- "help.audit_log": "Audit log",
- "help.auto_save": "Automatic save",
- "help.auto_scroll": "Auto-scroll",
- "help.autocomplete": "Autocomplete",
- "help.b6619ac9": "Ctrl+Alt+Space : Command palette (action list)",
- "help.backlinks_panel": "Backlinks panel",
- "help.backup_auto": "Auto backup",
- "help.before_write": "Before write",
- "help.breadcrumb": "Breadcrumb",
- "help.breadcrumb_clickable": "Clickable breadcrumb",
- "help.breadcrumb_reposition": "Breadcrumb and repositioning",
- "help.c017944b": "📄 Files",
- "help.c48fb18a": "Authentication",
- "help.c8532869": "Completion, rewrite, translation (see",
- "help.case_sensitive": "Case-sensitive",
- "help.cf8615b1": "Visualization",
- "help.ci_cd": "CI/CD automation",
- "help.clear": "Clear",
- "help.clear_search": "Clear",
- "help.click_segment": "Click a segment",
- "help.click_tab": "Click a tab",
- "help.click_vault": "Click the vault",
- "help.clickable": "Clickable",
- "help.close_help": "Close help",
- "help.close_tab_x": "✕ button",
- "help.code_preview": "Code + Preview",
- "help.command_palette": "Command palette",
- "help.configuration": "Settings",
- "help.content_area": "Content area",
- "help.content_shows": "The main area displays:",
- "help.context_actions": "File actions",
- "help.context_menu": "Context menu (right click)",
- "help.copy_path": "Copy path",
- "help.d186a19e": "The \"Recent\" tab in the sidebar lists your",
- "help.d1ea1dc6": "n8n / Make (no-code)",
- "help.d5c977ac": "Code blocks adapt to the theme",
- "help.dark": "Dark",
- "help.dashboard": "Dashboard statistics",
- "help.delete": "Delete",
- "help.desc_00b9572b": ":\n Results ranked by TF-IDF relevance",
- "help.desc_04493800": ": 64\n caractères hexadécimaux, impossible à\n deviner",
- "help.desc_065f53ee": ": Silent auto-save after 2s of inactivity, green confirmation flash",
- "help.desc_071da87a": "Lecture seule : La vue publique n'affiche que le contenu sans édition",
- "help.desc_0739c6a9": "Images : Support complet des syntaxes Obsidian",
- "help.desc_085ec31f": ":\n Quick select at the top of the Vault tabs",
- "help.desc_0f9a1e5b": "Use the sidebar to navigate between vaults and folders.",
- "help.desc_11cf9e8a": "Éditer ▾ — Menu de modification du texte sélectionné : 🪄 Improve writing : Améliore la qualité, la clarté et le flux du texte",
- "help.desc_13d206d4": "Transforms textual data into a readable Markdown table — ideal for comparisons and structured information.",
- "help.desc_13d90039": "ObsiGate generates a shareable, read-only link.",
- "help.desc_1577912b": "Rate limiting : 10 tentatives / 15 min par IP + verrouillage compte",
- "help.desc_170a6982": "Converts a sentence or paragraph into a structured bullet-point list.",
- "help.desc_188bc423": "Pour plus d'informations, consultez le dépôt du projet.",
- "help.desc_19cf5730": "Rotation",
- "help.desc_19cfa369": "Documents are automatically saved after each modification. A status indicator in the editor header confirms: Saved or Saving… .",
- "help.desc_1a04ae18": "Use the sidebar to navigate between vaults and folders.",
- "help.desc_1ba39cc4": "Update",
- "help.desc_1c130d89": "Translate — translates selected text to target language",
- "help.desc_1f11691c": "Atomic writes : Écriture via fichier temporaire + renommage atomique",
- "help.desc_2349271d": "Automatisation CI/CD : Déclencher un build quand une note de procédure est mise à jour",
- "help.desc_26f81732": "Édition rapide : Ouvrez l'éditeur, modifiez, sauvegardez",
- "help.desc_287be69c": "Vault",
- "help.desc_2c220763": "Edit config.yaml to customize behavior",
- "help.desc_2f2c16b5": "Publish",
- "help.desc_31e7253b": "Each backup",
- "help.desc_32e3d66d": "En cas d'erreur de clé API : AI: clé API invalide",
- "help.desc_39979e71": "Vault",
- "help.desc_39e97b2f": "Note: the file_modified event is reserved for future use and is not currently emitted.",
- "help.desc_3ace259e": "— Inline completion",
- "help.desc_3c4711b8": "Edit config.yaml to customize behavior",
- "help.desc_3d4e9189": "Simplifies the language for easier reading — replaces complex terms and shortens long sentences.",
- "help.desc_3da1896a": "Each webhook receives a POST with event details",
- "help.desc_3db2812f": "X-ObsiGate-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 Pour vérifier côté récepteur : calculez HMAC-SHA256(corps_brut, secret) et comparez. Le header X-ObsiGate-Event contient aussi le type d'événement pour un routage rapide.",
- "help.desc_40c92863": "Exploration par tags : Cliquez sur un tag dans une note pour voir toutes les notes similaires",
- "help.desc_42a81347": "Casual and friendly tone — ideal for informal communication or social media.",
- "help.desc_443b76b5": "Wikilinks : Naviguez entre notes comme dans Obsidian",
- "help.desc_47e1e10f": "Générer ▾ — Génère du contenu à partir de la sélection : ℹ️ Explain this : Explique le texte sélectionné de façon claire",
- "help.desc_4de7e318": ": The 50 most recent searches (localStorage)",
- "help.desc_50a33a05": "Generates YAML frontmatter for the current document (title, date, tags based on content).",
- "help.desc_5374685a": "Mobile-friendly : Utilisez le bouton hamburger sur mobile",
- "help.desc_5396ed9e": "Profitez de l'historique : Réutilisez vos recherches fréquentes",
- "help.desc_53bfaeca": "Navigation",
- "help.desc_5524e921": "Adapts the tone for a formal, business context — suitable for reports, emails, and official documents.",
- "help.desc_555e0305": "Pour changer de provider par défaut : AI_DEFAULT_PROVIDER=openrouter",
- "help.desc_56760d7f": "Each webhook receives a POST with a JSON body containing the event type, vault, path, and a timestamp.",
- "help.desc_58d2326f": "Use the sidebar to navigate between vaults and folders.",
- "help.desc_599f3242": "Log",
- "help.desc_5a53de53": "📏 Make shorter : Rend le texte plus concis tout en préservant l'information",
- "help.desc_5ab8c78d": ":\n Stay open until manually closed",
- "help.desc_5d0fbb81": ":\n Workers, TF-IDF boosts, prefix expansions",
- "help.desc_5e9a7fd9": "In the",
- "help.desc_5fc28b7d": "Created",
- "help.desc_625112fd": "Sélectionnez un contexte : Choisissez un vault pour réduire le bruit",
- "help.desc_660051b0": "Audit externe : Logger toutes les modifications dans une base de données externe",
- "help.desc_664b1d09": "Use the sidebar to navigate between vaults and folders.",
- "help.desc_68c2b170": "Rotation : Rotation automatique à 10 Mo",
- "help.desc_6902f20d": "Favorisez les opérateurs : tag: et vault: accélèrent la recherche",
- "help.desc_698d92ec": "Gestion de l'espace : Réduisez les sections Vaults/Tags selon vos besoins",
- "help.desc_6a5ddef2": "Explains the selected passage with an educational style — defines technical terms and adds context.",
- "help.desc_6a80241e": "Width",
- "help.desc_6ae5342c": "Index stats",
- "help.desc_6b649b1e": ": Highlighted in accent color",
- "help.desc_6b88a375": ":\n Debounce delay, results per page, timeout",
- "help.desc_6b8fc728": "Select",
- "help.desc_6bc48f59": "Each webhook receives a POST with event details",
- "help.desc_6caffeed": " — Conversion tools:",
- "help.desc_6d68fbc9": "Create markdown file: Create a new .md file",
- "help.desc_6ebebdfd": ": Each operator type has its color",
- "help.desc_6f7c5889": "Alert",
- "help.desc_71eeb16a": ":\n Réutilisez vos recherches fréquentes",
- "help.desc_723ed8db": "Non-root Docker : Conteneur tourne avec UID 1000",
- "help.desc_752789ab": "File",
- "help.desc_759926ee": "Relaxed and friendly tone — ideal for informal communication or social media.",
- "help.desc_7671e56a": "Avant écriture : Sauvegarde du contenu original dans .obsigate-backup/",
- "help.desc_7708f226": "ObsiGate uses JWT tokens signed with a secret key defined in the .env file.",
- "help.desc_7819b56f": "Each webhook receives a POST with event details",
- "help.desc_7ab5be50": "Translates the selected text into the target language.",
- "help.desc_7b45490b": "Each webhook receives a POST with event details",
- "help.desc_7e86626c": "ObsiGate generates a shareable, read-only link. Anyone with the link can view the file content.",
- "help.desc_7f1d0512": "Path traversal : Validation des chemins contre les attaques ../",
- "help.desc_7ff2fa21": "Pendant une requête AI, un toast ⏳ AI: traitement en cours... s'affiche",
- "help.desc_803161e5": "The toolbar contains the following buttons:",
- "help.desc_828db5da": ": Both types are combined",
- "help.desc_858681a1": ":\n Durée de vie en heures",
- "help.desc_85effb89": "Each webhook receives a POST with event details",
- "help.desc_8717768b": ": Node metadata (tags, links, path)",
- "help.desc_87b348c3": "Recherche multi-critères : tag:docker tag:linux vault:IT nginx",
- "help.desc_89a8f5de": "Example: publish a blog post when a file is marked publish: true.",
- "help.desc_89bd3c4c": ": Only shows nodes with a specific tag",
- "help.desc_8f0bffe3": "JWT tokens",
- "help.desc_9186a3a3": "Explorez l'arborescence : Naviguez dans les dossiers",
- "help.desc_93902d18": "Utilisez les tags : Filtrez par tags pour affiner",
- "help.desc_9846fc07": "In the",
- "help.desc_985d759b": "Token unique : 64 caractères hexadécimaux, impossible à deviner",
- "help.desc_991307d6": " — Generate content from the selection:",
- "help.desc_995a711f": ": Opens the CodeMirror editor",
- "help.desc_99c6f0ec": "Share files with a read-only link",
- "help.desc_9cb5974c": ": Ouvrez\n plusieurs notes dans des onglets séparés",
- "help.desc_9d0e39d0": "Headers CSP : Content-Security-Policy, X-Frame-Options, XSS-Protection",
- "help.desc_9edeaa11": ": Controls exploration depth (0-3)",
- "help.desc_9f321d1e": "Backup",
- "help.desc_9f78b3ec": ":\n Easier navigation in long files",
- "help.desc_9fec424d": "Adapts the tone for a professional context — reports, emails, official documents.",
- "help.desc_a0ec890a": " — Selected text modification menu:",
- "help.desc_a13f7f2c": "Une fois terminé, AI: texte traité ✓ confirme le succès",
- "help.desc_a4a4e21b": ": \"×\" button to reset the filter",
- "help.desc_a52fc3b1": ": Downloads the file",
- "help.desc_a579cf4d": ": Preview note content without navigating",
- "help.desc_a601f22e": "Each webhook receives a POST with event details",
- "help.desc_a69804cd": "Répertoire dédié : .obsigate-backup/ dans le répertoire de la vault",
- "help.desc_a6cb3b26": "🔤 Fix spelling & grammar : Corrige les fautes d'orthographe et de grammaire",
- "help.desc_a974f7a6": "Journal : Fichier data/audit.log au format JSON lines",
- "help.desc_a9c2c520": "Webhooks automatically notify external services when files or directories change. Configure a URL to receive HTTP POST requests.",
- "help.desc_a9db82b5": ": Centers the view on a specific node",
- "help.desc_ab2c4150": ": Files (squares) and folders (circles) with icons",
- "help.desc_action_buttons": "Copy, Source, Download, Edit, Open in new window",
- "help.desc_adv_syntax": ": Python, JS, HTML, CSS, Java, C++, Rust, Go and many more",
- "help.desc_ae213f54": ":\n Combinez opérateurs et texte libre",
- "help.desc_aec0ad2c": "💬 Réécrire — Réécrit le texte selon vos instructions personnalisées. Une boîte de dialogue vous demande votre instruction.",
- "help.desc_all_tags": ": Extracted from YAML frontmatters and inline (#tag)",
- "help.desc_all_vaults": ": Global view of all your content",
- "help.desc_autoscroll": ": The sidebar positions itself automatically",
- "help.desc_b54c21ef": "Contrôle d'accès par vault : Chaque utilisateur voit uniquement ses vaults",
- "help.desc_b5f89b3b": "ObsiGate integrates an AI assistant directly into the text editor. To activate it, configure at least one AI provider in the .env file (DeepSeek, OpenRouter, or Gemini).",
- "help.desc_b6169f2e": "Each webhook receives a POST with event details",
- "help.desc_b9c94994": ": Preview of the applied regex",
- "help.desc_ba4578ae": "Restart the container:",
- "help.desc_bc08f215": "ObsiGate uses JWT tokens signed with a secret key defined in the .env file.",
- "help.desc_beed5686": "Click on",
- "help.desc_breadcrumb": "File path (clickable to reposition sidebar)",
- "help.desc_c1f72521": "Edit config.yaml to customize behavior",
- "help.desc_c63f02cc": "Rewrites the text according to your custom instructions. A dialog prompts for your instruction.",
- "help.desc_c8532869": ": Completion, rewrite, translation (see",
- "help.desc_c992f2e5": "Remplissez : Nom (descriptif), URL (endpoint cible), Secret (optionnel)",
- "help.desc_case_sensitive": ": \"Aa\" button to respect case",
- "help.desc_cb6f0bfd": "Backup",
- "help.desc_cc8e35ba": "Notifications : Alerter un canal Slack/Discord quand un document est créé",
- "help.desc_cdf7bb46": "Recherchez intelligemment : Combinez opérateurs et texte libre",
- "help.desc_cf5ccd40": "Simplifies the language for easier reading — replaces complex terms and shortens long sentences.",
- "help.desc_cfe362a7": "Export PDF : Téléchargement du document partagé en PDF via/s/{token}/pdf",
- "help.desc_check_btn": ": Manual save",
- "help.desc_chip_remove": ": Click \"×\" to remove a filter",
- "help.desc_chip_view": ": See active filters at a glance",
- "help.desc_click_segment": ": Navigate to that folder",
- "help.desc_click_tab": ": Activates the tab",
- "help.desc_click_vault": ": Focus on the vault in the sidebar",
- "help.desc_close_btn": ": Closes the tab (unless it's the only one)",
- "help.desc_cm_autocomplete": ": Contextual suggestions",
- "help.desc_cm_lines": ": Easy navigation",
- "help.desc_cm_search": ": Ctrl+F to search in the file",
- "help.desc_cm_syntax": ": Supports many languages",
- "help.desc_combined": "searches \"web server\" in Markdown files of the IT vault with the linux tag.",
- "help.desc_context_menu": ": Right-click on a vault or folder → Graph View",
- "help.desc_copy_btn": ": Copies source content to clipboard",
- "help.desc_ctrl_tab": ": Next tab",
- "help.desc_ctrl_w": ": Close the active tab",
- "help.desc_d186a19e": ": The Recent tab in the sidebar lists your latest files",
- "help.desc_d3a71d97": "file_modified n'est pas encore dispatché (réservé pour version future)",
- "help.desc_d5c977ac": "Code blocks",
- "help.desc_d646b709": "Compte admin : Créé automatiquement au premier démarrage",
- "help.desc_d66e911c": "In the",
- "help.desc_d915c72b": "Synchronisation : Copier les nouveaux fichiers vers un autre système (SharePoint, Google Drive)",
- "help.desc_d9cdaa89": "Copy",
- "help.desc_d9e8ab0b": "🧰 Boîte ▾ — Outils de conversion : 📋 Convert to list : Transforme le texte en liste Markdown",
- "help.desc_dc4d7186": "Each webhook receives a POST with event details",
- "help.desc_delete_btn_new": ": Trash button to delete",
- "help.desc_double_click": ": Opens the file in a persistent tab",
- "help.desc_e0525213": "Inline completion (Ctrl+J ). Automatically completes the text where the cursor is located.",
- "help.desc_e23c94dd": ": In the toolbar (when available)",
- "help.desc_e36f3eaa": "📐 Make longer : Ajoute des détails, exemples ou explications",
- "help.desc_e3fce7a3": ": Opens the file in a temporary tab (preview, italic)",
- "help.desc_e47d2328": "Preview",
- "help.desc_e5246734": ": Previous tab",
- "help.desc_e5b67e3d": "Your choice",
- "help.desc_e5cf90b2": "Creates a concise summary of the selected text — bullet points capturing the essentials.",
- "help.desc_e6129268": "Use the sidebar to navigate between vaults and folders.",
- "help.desc_e7e3157a": "Timestamp : Chaque backup est horodaté",
- "help.desc_e93517a5": "Download",
- "help.desc_ec5f7a26": "Edit config.yaml to customize behavior",
- "help.desc_ecf302ae": "Multi-fenêtres : Ouvrez plusieurs notes dans des onglets séparés",
- "help.desc_edges": ": Outgoing wikilinks (blue) and incoming backlinks (green)",
- "help.desc_eebf177f": "ObsiGate v1.7.0 - Porte d'entrée web pour vos vaults Obsidian",
- "help.desc_ef1531ce": ": A temporary tab (italic) is replaced by the next click",
- "help.desc_ef3f8158": ": Manage your filtering patterns",
- "help.desc_examples": "Examples : ext:sh searches in bash scripts, ext:py in Python scripts, ext:md in Markdown files.",
- "help.desc_expand": ": Click on a folder",
- "help.desc_export_png": ": Save the graph as an image",
- "help.desc_f0adf67a": ": Scroll wheel to zoom, click-drag to move",
- "help.desc_f2d4e369": "✏️ Continue writing : Continue l'écriture à partir du texte",
- "help.desc_f53badcd": "Add your API key in .env:",
- "help.desc_f58da6e1": "Cliquez Ajouter",
- "help.desc_f9083ce5": "File",
- "help.desc_f952db17": "Refresh tokens : Cookies HttpOnly avec rotation automatique",
- "help.desc_fa126609": "Converts the document structure into an Obsidian Canvas (visual cards).",
- "help.desc_facet_vaults": ": Counters by vault",
- "help.desc_fc9391f3": "ObsiGate sends an HTTP POST to your webhook URL with event details.",
- "help.desc_fdeaf803": ": Each file type has its icon (markdown, code, etc.)",
- "help.desc_file_suggest": ": File title suggestions",
- "help.desc_filter_type": ": Include/exclude files, folders, markdown, other",
- "help.desc_filter_usage": "Use the filter bar at the top of the sidebar to search quickly:",
- "help.desc_filtering": "Search bar to filter files/tags",
- "help.desc_find_in_page": ": Ctrl+F to search in the file (case, regex, whole word)",
- "help.desc_frontmatter": ": tags: [docker, linux] or tags: docker, linux",
- "help.desc_fullscreen": ": Button to expand the view",
- "help.desc_graph_intro": "The graph view offers an interactive visualization of connections between your notes via wikilinks.",
- "help.desc_graph_nav": ": Navigation history between nodes",
- "help.desc_grouped_results": ": Display by vault with full paths",
- "help.desc_hamburger": "(mobile) : Show/hide the sidebar",
- "help.desc_hide": "Click the sidebar icon in the header",
- "help.desc_images": ": Supports ![[image.png]]",
- "help.desc_inline_tags": ": #docker in markdown content",
- "help.desc_instant_filter": ": Type to filter files and folders",
- "help.desc_logo": "Return to home",
- "help.desc_markdown_all": ": Full markdown rendering",
- "help.desc_md_render": "Markdown files are rendered with:",
- "help.desc_menu_header": ": \"Vault\" dropdown in the Options menu",
- "help.desc_menu_options": ": Access to settings, theme, help",
- "help.desc_op_ext": ": Filter by file type",
- "help.desc_op_path": ": Filter by path",
- "help.desc_op_phrase": ": Exact phrase search",
- "help.desc_op_tag": ": Filter by tag",
- "help.desc_op_title": ": Search in titles only",
- "help.desc_op_vault": ": Search in a specific vault",
- "help.desc_open_file": ": Click on the file name",
- "help.desc_open_new": ": Opens the file in a new tab",
- "help.desc_patterns": ": Ex: #<% ... %> or #{{ ... }}",
- "help.desc_quick_select": "Dropdown to change vault",
- "help.desc_rendered_content": "Markdown with wikilinks and images",
- "help.desc_resize": "Drag the right edge to adjust width",
- "help.desc_save_btn_new": ": ✓ button to save",
- "help.desc_search_all": ": Title, content, tags, paths",
- "help.desc_search_bar": "Global search with autocomplete",
- "help.desc_search_intro": "Type in the top search bar to start a fulltext search:",
- "help.desc_sidebar_toggle": "Hide the sidebar for more space",
- "help.desc_snippets": ": Excerpts with highlighted terms",
- "help.desc_source_btn": ": Shows/hides raw markdown",
- "help.desc_specific_vault": ": Focus on a single vault",
- "help.desc_syntax_hl": ": Code block highlighting",
- "help.desc_tag_cloud_intro": "The Tags tab in the sidebar displays a tag cloud:",
- "help.desc_tag_filter_use": ": Use the filter bar to search for a tag",
- "help.desc_tag_suggest": ": Tag suggestions with counters",
- "help.desc_tags_tab": "Clickable tag cloud",
- "help.desc_template_intro": "In the Configurations, you can hide template tags:",
- "help.desc_title_tags": "File metadata",
- "help.desc_trash_btn": ": Delete file",
- "help.desc_two_tag_types": "ObsiGate supports two types of tags:",
- "help.desc_vault_context": "Change context to filter the display:",
- "help.desc_vault_indicator": "Shows the active context (All, vault1, etc.)",
- "help.desc_vaults_tab": "File and folder tree",
- "help.desc_visual_search": ": Search bar to highlight nodes",
- "help.desc_wikilinks": ": [[link]] and [[link|text]]",
- "help.double_click": "Double click",
- "help.drag_drop_tabs": "Drag & drop tabs",
- "help.e23c94dd": "In the toolbar (when available)",
- "help.e3fce7a3": "Opens the file in a temporary tab (preview,",
- "help.e5b67e3d": "Your choice is saved in localStorage",
- "help.editor_section": "Editor",
- "help.ef1531ce": "A temporary tab (italic) is replaced",
- "help.experience": "Experience",
- "help.export": "Export",
- "help.external_audit": "External audit",
- "help.f0adf67a": "Scroll wheel to zoom, click-drag to move",
- "help.fallback_editor": "Fallback editor",
- "help.fdeaf803": "Each file type has its icon (markdown, code, etc.)",
- "help.fe388b67": "Optimizations",
- "help.file_actions": "Actions on the open file:",
- "help.file_viewer": "File viewer",
- "help.filter_toggles": "Toggles Aa, wd, .*",
- "help.filtering": "Filtering",
- "help.find_capabilities": "Regex, whole word, case-sensitive search in the displayed document with DOM TreeWalker.",
- "help.find_in_page": "Find in page",
- "help.footer": "© 2026 — Made with ☕, 🎧 and a few",
- "help.graph_context": "Context menu",
- "help.graph_depth": "Depth slider",
- "help.graph_export": "Export PNG",
- "help.graph_filter_depth": "Filters & depth",
- "help.graph_focus": "Focus mode",
- "help.graph_fullscreen": "Full screen & PNG export",
- "help.graph_info": "Info panel",
- "help.graph_nav_arrows": "Navigation ← → ↑",
- "help.graph_navigation": "History navigation ← → ↑",
- "help.graph_nodes": "Nodes",
- "help.graph_preview": "Ctrl+hover",
- "help.graph_sticky": "Sticky panels",
- "help.graph_tag_filter": "Filter by tag",
- "help.graph_type_filter": "Filter by type",
- "help.graph_view": "Graph view",
- "help.graph_visual": "Visual search",
- "help.graph_zoom": "Zoom/Pan",
- "help.graph_zoom_pan": "Scroll wheel zoom & pan",
- "help.guide_official": "Official guide",
- "help.hamburger_btn": "Hamburger menu button",
- "help.header_section": "Header",
- "help.help_title": "Help",
- "help.hide": "Hide",
- "help.hide_help": "Hide help",
- "help.highlighted_snippets": "Highlighted snippets",
- "help.indexes": "ObsiGate indexes and displays:",
- "help.instant": "Instant",
- "help.light": "Light",
- "help.logo": "ObsiGate logo",
- "help.markdown": "Markdown syntax",
- "help.menu_header": "Header menu",
- "help.menu_options": "Options menu",
- "help.mermaid_section": "Mermaid diagrams",
- "help.mermaid_templates": "Templates",
- "help.mobile": "Floating results bar",
- "help.modal_title": "ObsiGate User Guide",
- "help.multi_vault": "Multi-vault",
- "help.nav_ai": "🤖 AI Editor",
- "help.nav_backups": "💾 Backups",
- "help.nav_customization": "🎨 Customization",
- "help.nav_editing": "✏️ Editing",
- "help.nav_files": "📁 Files",
- "help.nav_graph": "🗺️ Graph",
- "help.nav_interface": "🧭 Interface",
- "help.nav_intro": "📘 Introduction",
- "help.nav_navigation": "🗺️ Navigation",
- "help.nav_palette": "⌨️ Palette",
- "help.nav_search": "🔍 Search",
- "help.nav_security": "🛡️ Security",
- "help.nav_sharing": "🔗 Sharing",
- "help.nav_shortcuts": "⌨️ Shortcuts",
- "help.nav_tabs": "📑 Tabs",
- "help.nav_tags": "🏷️ Tags",
- "help.nav_tips": "💡 Tips",
- "help.nav_webhooks": "🪝 Webhooks",
- "help.new_file": "New file",
- "help.new_folder": "New folder",
- "help.nights": "sleepless nights 😅",
- "help.nodes_edges": "Edges",
- "help.other": "Other",
- "help.outline_panel": "Outline panel (right)",
- "help.pagination": "Pagination",
- "help.pal_close_the_palette": "Close the palette",
- "help.pal_delete_a_folder_and_its_contents": "Delete a folder and its contents",
- "help.pal_delete_the_file_from_the_active_tab": "Delete the file from the active tab",
- "help.pal_move_desc": "Navigate the list",
- "help.pal_open___select": "Open / select",
- "help.pal_open_the_editor_on_the_active_tab": "Open the editor on the active tab",
- "help.pal_open_this_guide": "Open this guide",
- "help.pal_popout_desc": "Open the document in a detached window",
- "help.pal_toggle_light_dark": "Toggle light/dark",
- "help.pdf_export": "PDF export",
- "help.persistent_display": "Persistent display in bar",
- "help.persistent_tabs": "Persistent tabs",
- "help.popout_button": "Dedicated button",
- "help.powered_by": "Powered by Python, FastAPI, vanilla JS, CodeMirror, Mermaid, Lucide Icons and ❤️ for Obsidian.",
- "help.preview_tab": "Preview",
- "help.properties": "Properties",
- "help.public_shares": "Public sharing",
- "help.pwa_section": "PWA",
- "help.quick_help": "Quick Help",
- "help.quick_select": "Quick select",
- "help.rate_limiting": "Rate limiting",
- "help.recent_nav": "Recent navigation",
- "help.rename": "Rename",
- "help.rendered_content": "Rendered content",
- "help.rendering_images": "Image rendering",
- "help.rendering_md": "Rendering in Markdown",
- "help.resize": "Resize",
- "help.results_counter": "Results counter",
- "help.results_grouped": "Grouped results",
- "help.search_bar": "Search bar",
- "help.search_dropdown": "Search dropdown",
- "help.search_everywhere": "Search everywhere",
- "help.search_facets": "Facets",
- "help.search_filters": "Search filters",
- "help.search_history": "Search history",
- "help.search_operators": "Search operators",
- "help.search_placeholder": "Search help...",
- "help.search_save": "Saved searches",
- "help.secret_redaction": "Secret redaction",
- "help.security_section": "Security",
- "help.share_button": "Share button",
- "help.shortcut_ai_completion": "Inline AI completion (in editor)",
- "help.shortcut_close": "Close editor / modal",
- "help.shortcut_close_editor": "Close editor / modal",
- "help.shortcut_close_palette": "Close palette",
- "help.shortcut_close_tab": "Close tab",
- "help.shortcut_command_palette": "Command palette",
- "help.shortcut_file_palette": "File palette (quick nav.)",
- "help.shortcut_focus_search": "Focus search bar",
- "help.shortcut_forge_help": "Forge editor help",
- "help.shortcut_next_tab": "Next tab",
- "help.shortcut_palette": "Command palette",
- "help.shortcut_palette_cmd": "Command palette (command mode)",
- "help.shortcut_prev_tab": "Previous tab",
- "help.shortcut_quick_insert": "Quick insert (Forge editor)",
- "help.shortcut_quick_search": "Quick search (outside text field)",
- "help.shortcut_redo": "Redo",
- "help.shortcut_save": "Save",
- "help.shortcut_save_file": "Save open file",
- "help.shortcut_search_doc": "Search in document",
- "help.shortcut_select": "Open/select",
- "help.shortcut_tip": "Hover over a document with Ctrl for a quick preview. Click Forge in the toolbar for the advanced editor.",
- "help.shortcut_undo": "Undo",
- "help.shortcuts": "Keyboard shortcuts",
- "help.shortcuts_title": "Shortcuts & Tips",
- "help.sidebar_dropdown": "Sidebar dropdown",
- "help.sidebar_features": "Sidebar features:",
- "help.sidebar_filtering": "Sidebar filtering",
- "help.sidebar_section": "Sidebar",
- "help.sidebar_tabs": "The sidebar has two tabs:",
- "help.sidebar_toggle_btn": "Sidebar toggle button",
- "help.simple": "Simple",
- "help.simple_search": "Simple search",
- "help.single_click": "Single click",
- "help.smart_scoring": "Smart scoring",
- "help.sort_options": "Sort options",
- "help.supported_languages": "Many languages supported",
- "help.syncthing": "Syncthing conflicts",
- "help.syncthing_conflicts": "Syncthing conflicts",
- "help.tab_bar": "Tab bar",
- "help.tab_features": "Features",
- "help.tab_nav": "Tab navigation",
- "help.tabs_section": "Opening tabs",
- "help.theme_section": "Theme",
- "help.theme_sync": "Synced theme",
- "help.theme_toggle": "Toggle between themes:",
- "help.themes": "Themes",
- "help.tip_hide_sidebar": "Hide the sidebar",
- "help.tip_sidebar_filter": "Use sidebar filtering",
- "help.tip_use_tags": "Use tags to organize",
- "help.title": "ObsiGate Help",
- "help.title_tags": "Title and tags",
- "help.toc_title": "CONTENTS",
- "help.toolbar_section": "AI toolbar",
- "help.tree_section": "Vault tree",
- "help.use_cases": "Use cases",
- "help.vault_indicator": "Vault indicator",
- "help.vault_selector": "Vault selector",
- "help.vault_specific": "Specific vault",
- "help.webhook_blog_post": "blog post when a file is marked",
- "help.webhook_note": "Note: the event",
- "help.webhook_notify": "automatically notify services",
- "help.webhook_post": "HTTP POST to your",
- "help.webhooks": "Webhooks",
- "help.welcome_desc": "ObsiGate is your web gateway to your Obsidian vaults. Access, browse, and search all your notes from any device via a modern and responsive interface.",
- "help.welcome_tag1": "⚡ Fast navigation",
- "help.welcome_tag2": "🗂️ Multi-vault",
- "help.welcome_tag3": "🏷️ Smart tags",
- "help.welcome_tag4": "📝 Reading & editing",
- "help.welcome_tag5": "🔍 TF-IDF search",
- "help.welcome_title": "Welcome to ObsiGate",
- "help.wikilinks": "Wikilinks",
- "help.zoom_pan": "Zoom & Pan",
- "mermaid.code": "Code",
- "mermaid.copy_svg": "Copy SVG",
- "mermaid.download_png": "Download PNG",
- "mermaid.preview": "Preview",
- "mermaid.zoom_in": "Zoom in",
- "mermaid.zoom_out": "Zoom out",
- "mobile.all_vaults": "📂 All vaults",
- "modal.create_btn": "Create",
- "modal.create_file": "Create a file",
- "modal.creating": "Creating...",
- "modal.delete_dir": "Delete folder",
- "modal.delete_dir_warning": "All files and subfolders will be permanently deleted.",
- "modal.delete_file": "Delete file",
- "modal.delete_irreversible": "This action is irreversible.",
- "modal.delete_permanently": "Delete permanently",
- "modal.delete_warning": "Warning!",
- "modal.dir_to_delete": "Folder to delete:",
- "modal.file_name": "File name",
- "modal.file_to_delete": "File to delete:",
- "modal.file_type": "File type",
- "modal.in_folder": "In: {path}",
- "month.april": "April",
- "month.august": "August",
- "month.december": "December",
- "month.february": "February",
- "month.january": "January",
- "month.july": "July",
- "month.june": "June",
- "month.march": "March",
- "month.may": "May",
- "month.november": "November",
- "month.october": "October",
- "month.september": "September",
- "offline.all_synced": "All changes synced",
- "offline.conflict_title": "Sync conflict",
- "offline.keep_local": "Keep local version",
- "offline.keep_server": "Keep server version",
- "offline.mode_offline": "Offline mode — changes will sync when back online",
- "offline.pending": "{count} pending change(s)",
- "offline.reconnected": "Connection restored — synchronizing...",
- "offline.status": "Offline",
- "offline.sync_now": "Sync now",
- "palette.browse_dirs": "📂 Browse directories...",
- "palette.browse_hint": "Navigate vaults to find a file",
- "palette.browse_in": "In {path}",
- "palette.browse_no_results": "No results — browse manually",
- "palette.browse_open_file": "📂 Open a file in this directory",
- "palette.browse_root": "root",
- "palette.close_current": "Close current file",
- "palette.close_current_desc": "Close active tab without deleting",
- "palette.cmd_back_home": "🏠 Home",
- "palette.cmd_back_home_desc": "Back to home",
- "palette.cmd_config": "⚙️ Settings",
- "palette.cmd_config_desc": "Open settings",
- "palette.cmd_create_dir": "📁 Create directory",
- "palette.cmd_create_dir_desc": "Choose vault, navigate, then name the directory",
- "palette.cmd_create_file": "📄 Create markdown file",
- "palette.cmd_create_file_desc": "Choose vault, navigate, then name the file",
- "palette.cmd_delete_dir": "🗑️ Delete directory",
- "palette.cmd_delete_dir_desc": "Choose vault, navigate and delete",
- "palette.cmd_theme": "🌓 Toggle theme",
- "palette.cmd_theme_desc": "Toggle light/dark",
- "palette.del_all_content": "All content will be deleted",
- "palette.delete_current": "Delete current file",
- "palette.delete_current_desc": "Delete file from active tab",
- "palette.delete_file_desc": "Search for a file to delete",
- "palette.dir_delete_confirm": "All content will be deleted",
- "palette.dir_name": "Directory name",
- "palette.empty_dir": "📂 Empty directory",
- "palette.error_loading": "Loading error",
- "palette.error_prefix": "Error: {msg}",
- "palette.file_name": "File name",
- "palette.help_desc": "Open the guide",
- "palette.md_hint": "(.md added if needed)",
- "palette.mode_commands": "Commands",
- "palette.mode_files": "Files",
- "palette.no_results": "No results for \"{query}\"",
- "palette.no_vaults": "No vaults found",
- "palette.popout_desc": "Open document in a window",
- "palette.reindex_desc": "Force full reindex",
- "palette.search_commands": "Type a command...",
- "palette.search_error": "Search error",
- "palette.search_files": "Search files...",
- "palette.type_command": "Type a command...",
- "palette.type_search": "Search files... (+include -exclude)",
- "palette.vault_count": "{count} files",
- "pwa.dismiss": "Later",
- "pwa.install_button": "Install",
- "pwa.install_desc": "Install this app on your device for quick access.",
- "pwa.install_title": "Install ObsiGate",
- "search.advanced_operators": "Advanced operators",
- "search.aria_label": "Search suggestions",
- "search.case_sensitive": "Case sensitive",
- "search.clear": "Clear",
- "search.dropdown_clear_history": "Clear history",
- "search.dropdown_files": "Files",
- "search.dropdown_history": "History",
- "search.error": "Search error",
- "search.exclude_placeholder": "Exclude: vendor/*, *.lock",
- "search.facet_tags": "Tags",
- "search.facet_vaults": "Vaults",
- "search.files_filtered": "{count} file(s) with filters",
- "search.filter_path_title": "Path filters (Alt-F)",
- "search.filters": "Filters",
- "search.full_text": "Search everywhere",
- "search.highlighted_snippets": "Highlighted snippets",
- "search.include_placeholder": "Include: **/*.md, notes/**",
- "search.loading": "Searching...",
- "search.no_results": "No results found",
- "search.operators_help": "Operators: tag:word, vault:name, title:word, path:word, ext:md",
- "search.pagination": "Pagination",
- "search.previous": "← Previous",
- "search.query_time": "({time} ms)",
- "search.regex": "Regular expression",
- "search.results_count": "{count} result(s)",
- "search.results_filtered": "{count} result(s) for \"{query}\" with filters",
- "search.results_for": "{count} result(s) for \"{query}\"",
- "search.save_search": "Save search",
- "search.saved": "Search saved",
- "search.sections_all_vaults": "All vaults",
- "search.simple_search": "Simple search",
- "search.smart_scoring": "Smart scoring",
- "search.sort_date": "Date",
- "search.sort_relevance": "Relevance",
- "search.sort_title": "Title",
- "search.whole_word": "Whole word",
- "settings.about": "📦 About",
- "settings.ai": "🤖 AI",
- "settings.backend": "⚙️ Backend",
- "settings.backend_hint": "These settings are saved on the server. Some require a restart or reindexing.",
- "settings.backend_section": "Backend Settings",
- "settings.client_label": "These settings apply immediately on the client side.",
- "settings.diagnostics": "🩺 Diagnostics",
- "settings.history_count_desc": "Between 5 and 100 files (saved on server)",
- "settings.history_count_label": "Files in history",
- "settings.history_section": "Recent History",
- "settings.no_restart_badge": "No restart needed",
- "settings.profile": "👤 Profile",
- "settings.reindex": "Force reindex",
- "settings.restart_badge": "Restart required",
- "settings.save": "Save",
- "settings.search": "Search",
- "settings.tabs": "Tabs",
- "settings.search_placeholder": "Search...",
- "settings.sync": "🔄 Sync",
- "settings.themes": "🎨 Themes",
- "share.copied": "Link copied!",
- "share.copy_link": "Copy link",
- "share.create": "Create share link",
- "share.expires": "Expires on",
- "share.never": "Never",
- "share.revoke": "Revoke",
- "share.title": "Public share",
- "shortcut.ai_completion": "AI inline completion (in editor)",
- "shortcut.close_editor": "Close editor / modal",
- "shortcut.command_palette": "Command palette",
- "shortcut.editor_help": "Forge editor help",
- "shortcut.file_palette": "File palette (quick nav)",
- "shortcut.focus_search": "Focus search bar",
- "shortcut.quick_insert": "Quick insert (Forge editor)",
- "shortcut.quick_search": "Quick search (outside text field)",
- "shortcut.save_file": "Save open file",
- "shortcut.search_in_doc": "Search in document",
- "shortcut.tip_preview": "Hover a document with **Ctrl** for a quick preview. Click **Forge** in the toolbar for the advanced editor.",
- "sidebar.all_vaults": "All vaults",
- "sidebar.case_sensitive": "Case sensitive",
- "sidebar.clear_filter": "Clear",
- "sidebar.expand_collapse": "Expand/Collapse",
- "sidebar.file_icons": "Icons",
- "sidebar.filter_instant": "Instant search",
- "sidebar.filter_path": "Path filters",
- "sidebar.filter_placeholder": "Filter files...",
- "sidebar.filter_results_grouped": "Grouped results",
- "sidebar.loading": "Loading...",
- "sidebar.no_recent_files": "No recent files",
- "sidebar.no_results": "No matching directory or file.",
- "sidebar.no_saved_searches": "No saved searches",
- "sidebar.open_file": "Open file",
- "sidebar.saved_dirs_tab": "Folders",
- "sidebar.saved_searches_tab": "Searches",
- "sidebar.sidebar_dropdown": "Vault context menu",
- "sidebar.sidebar_filtering": "Sidebar filtering",
- "sidebar.tab_backups": "Backups",
- "sidebar.tab_recent": "Recent",
- "sidebar.tab_saved": "Searches",
- "sidebar.tab_tags": "Tags",
- "sidebar.tab_vaults": "Vaults",
- "sidebar.vault_context_all": "All vaults",
- "sidebar.vault_specific": "Specific vault",
- "sync.autocomplete": "Autocomplete",
- "sync.autocomplete_active": "Autocomplete active",
- "sync.connected": "Synchronized",
- "sync.connected_short": "Connected",
- "sync.disconnected": "Disconnected",
- "sync.event_index_progress": "Vault indexed",
- "sync.event_index_start": "Index starting",
- "sync.event_index_updated": "Update",
- "sync.event_vault_added": "Vault added",
- "sync.event_vault_removed": "Vault removed",
- "sync.index_complete": "Indexing complete ({count} files)",
- "sync.index_reloaded": "Full index reloaded",
- "sync.index_started": "Indexing started ({count} vaults)",
- "sync.install_success": "ObsiGate installed successfully!",
- "sync.model_not_loaded": "Model not loaded",
- "sync.no_provider": "No provider",
- "sync.no_recent_events": "No recent events",
- "sync.not_configured": "Not configured",
- "sync.processing": "Processing…",
- "sync.update": "Update",
- "sync.updating": "Updating...",
- "sync.vault_added": "Vault \"{vault}\" added ({count} files)",
- "sync.vault_added_label": "Vault added",
- "sync.vault_files": "{vault} ({files} files)",
- "sync.vault_indexed_label": "Vault indexed",
- "sync.vault_removed": "Vault \"{vault}\" removed",
- "sync.vault_removed_label": "Vault removed",
- "sync.verifying": "Verifying…",
- "theme.change": "Change theme",
- "theme.dark": "Dark",
- "theme.light": "Light",
- "theme.title": "Theme",
- "themes.name_gradients": "Subtle gradients",
- "themes.name_terminal": "Pure black & green",
- "toast.ai_keys_saved": "API keys saved",
- "toast.ai_no_key": "No keys to save",
- "toast.bookmark_added": "Added to bookmarks",
- "toast.bookmark_removed": "Removed from bookmarks",
- "toast.cannot_delete_self": "Cannot delete your own account",
- "toast.config_client_saved": "Client settings saved",
- "toast.config_reset": "Configuration reset",
- "toast.config_saved": "Configuration saved",
- "toast.conflict_resolved": "Conflict resolved",
- "toast.copied": "Copied!",
- "toast.copy_error": "Copy error",
- "toast.create_error": "Creation error",
- "toast.created": "Created",
- "toast.delete_error": "Deletion error",
- "toast.deleted": "Deleted",
- "toast.dir_created": "Folder \"{name}\" created",
- "toast.dir_deleted": "Folder deleted ({count} files)",
- "toast.dir_renamed": "Directory renamed",
- "toast.dir_saved_search": "Folder saved to searches",
- "toast.error_delete": "Delete error",
- "toast.error_init": "Initialization error",
- "toast.error_load": "Loading error",
- "toast.error_network": "Network error",
- "toast.error_rename": "Cannot rename this item",
- "toast.file_created": "File \"{name}\" created",
- "toast.file_deleted": "File deleted",
- "toast.file_renamed": "File renamed",
- "toast.hidden_saved": "✓ Settings saved",
- "toast.index_reloaded": "Full index reloaded",
- "toast.installed": "ObsiGate installed successfully!",
- "toast.link_created": "Link created!",
- "toast.moved": "Moved",
- "toast.no_open_file": "No file open",
- "toast.not_found_tree": "Item not found in tree",
- "toast.path_copied": "Path copied: {path}",
- "toast.reindex_done": "Reindexing complete",
- "toast.reindex_started": "Reindexing...",
- "toast.rename_error": "Rename error",
- "toast.renamed": "Renamed",
- "toast.saved": "Saved",
- "toast.search_saved": "Search saved",
- "toast.share_revoked": "Share revoked",
- "toast.url_required": "URL required",
- "toast.user_created": "User created",
- "toast.user_deleted": "User deleted",
- "toast.user_modified": "User modified",
- "toast.viewer_error": "Viewer loading error",
- "toc.hide_panel": "Hide panel",
- "toc.no_headings": "No headings in this document",
- "toc.title": "ON THIS PAGE",
- "tree.new_file": "New file",
- "tree.new_folder": "New folder",
- "tree.vault_home": "Vault home",
- "ui.backups_versions": "Backups / Versions",
- "ui.cannot_rename": "Cannot rename this item",
- "ui.delete_name_empty": "Name cannot be empty",
- "ui.element_not_found": "Element not found in tree",
- "ui.vault_readonly": "Read-only vault",
- "vault.add_dynamic": "Add vault",
- "vault.reindex": "Reindex vault",
- "vault.remove_dynamic": "Remove vault",
- "vaulthome.empty": "No files in this directory",
- "vaulthome.error_loading": "Loading error:",
- "vaulthome.file_count": "{count} file",
- "vaulthome.file_count_plural": "{count} files",
- "vaulthome.loading": "Loading files...",
- "vaulthome.size_bytes": "B",
- "vaulthome.size_kb": "KB",
- "vaulthome.size_mb": "MB",
- "viewer.backlinks": "Backlinks",
- "viewer.binary_download": "Download file",
- "viewer.binary_unsupported": "Unsupported binary file",
- "viewer.bookmark_add": "Add to bookmarks",
- "viewer.bookmark_remove": "Remove bookmarks",
- "viewer.bookmark_toggle": "Add/Remove bookmarks",
- "viewer.breadcrumb_home": "Home",
- "viewer.click_to_download": "Click to download",
- "viewer.copied": "Copied!",
- "viewer.copy": "Copy",
- "viewer.copy_error": "Copy error",
- "viewer.download": "Download",
- "viewer.download_md": "Download as .md",
- "viewer.download_pdf": "Download as PDF",
- "viewer.edit": "Edit",
- "viewer.error": "Loading error",
- "viewer.forge_brand": "Forge",
- "viewer.forge_title": "Forge (new editor)",
- "viewer.index_start": "Starting index...",
- "viewer.index_updated": "Updated",
- "viewer.loaded_from_cache": "File loaded from offline cache",
- "viewer.loading": "Loading file...",
- "viewer.markdown": "Markdown",
- "viewer.metadata_created": "Created",
- "viewer.metadata_modified": "Modified",
- "viewer.metadata_path": "Path",
- "viewer.metadata_size": "Size",
- "viewer.metadata_tags": "Tags",
- "viewer.metadata_vault": "Vault",
- "viewer.no_headings": "No headings in this document",
- "viewer.on_this_page": "ON THIS PAGE",
- "viewer.outline": "Outline",
- "viewer.pdf_pages": "{count} pages",
- "viewer.popout": "Open in popup",
- "viewer.raw": "Raw",
- "viewer.share": "Share",
- "viewer.shared_doc_title": "Shared document — click to manage",
- "viewer.source": "Source",
- "viewer.syncing": "Updating...",
- "viewer.toc": "Table of contents",
- "viewer.unsupported_file": "This file is binary and cannot be displayed",
- "webhook.trigger_create": "Creation via API or palette",
- "webhook.trigger_delete": "Deletion via API or editor",
- "webhook.trigger_dir_create": "Directory creation via API",
- "webhook.trigger_dir_delete": "Directory deletion via API",
- "webhook.trigger_dir_rename": "Directory rename via API",
- "webhook.trigger_rename": "Rename via API"
-}
diff --git a/desktop/frontend-static/locales/fr.json b/desktop/frontend-static/locales/fr.json
deleted file mode 100644
index 0e2e44e..0000000
--- a/desktop/frontend-static/locales/fr.json
+++ /dev/null
@@ -1,1499 +0,0 @@
-{
- "_meta": {
- "locale": "fr",
- "name": "Français"
- },
- "about.api_version": "Version API",
- "about.backend": "Backend",
- "about.browser": "Navigateur",
- "about.built_with": "Construit avec",
- "about.editor": "Éditeur",
- "about.file_watcher": "Surveillance fichiers",
- "about.files": "Fichiers",
- "about.files_indexed": "Fichiers indexés",
- "about.footer": "Fait avec ☕, 🍕 et quelques git push --force accidentels.",
- "about.frontend": "Frontend",
- "about.fun_quote": "« Aucun LLM n'a été blessé durant ce développement. »",
- "about.git": "Git : — ",
- "about.icons": "Icônes",
- "about.joke_0": "0 bug connu. Les autres sont timides.",
- "about.joke_1": "Compile du premier coup. (Sauf le lundi matin.)",
- "about.joke_2": "Écrit par un humain. Probablement.",
- "about.joke_3": "Aucun LLM n'a été blessé durant ce développement.",
- "about.joke_4": "Quand ça marche, on appelle ça une feature.",
- "about.joke_5": "CSS: 90% du temps de dev, 10% du code.",
- "about.joke_6": "Dark mode activé depuis la naissance du projet.",
- "about.joke_7": "Ça marche sur ma machine™",
- "about.joke_8": "Techniquement, tout est un edge case.",
- "about.joke_9": "Le CSS de ce panneau a pris plus de temps que le backend.",
- "about.language": "Langue",
- "about.last_delivery": "Dernière livraison : Juin 2026 ",
- "about.made_with": "Fait avec ☕, 🎧 et quelques git push --force accidentels.",
- "about.markdown_renderer": "Rendu Markdown",
- "about.name": "Nom",
- "about.platform": "Plateforme",
- "about.section_app": "Application",
- "about.section_components": "Composants",
- "about.section_env": "Environnement",
- "about.status": "Statut",
- "about.subtitle": "Vos notes Obsidian, simplement accessibles.",
- "about.syntax_highlight": "Coloration syntaxe",
- "about.tagline": "Vos notes Obsidian, simplement accessibles.",
- "about.tests": "Tests",
- "about.vaults_configured": "Vaults configurés",
- "about.version": "Version",
- "admin.close": "Fermer",
- "admin.col_actions": "Actions",
- "admin.col_last_login": "Dernière connexion",
- "admin.col_role": "Rôle",
- "admin.col_status": "Statut",
- "admin.col_user": "Utilisateur",
- "admin.col_vaults": "Vaults",
- "admin.new_user": "+ Nouvel utilisateur",
- "admin.title": "Administration — Utilisateurs",
- "ai.casual": "Ton décontracté",
- "ai.completion_added": "AI: complétion ajoutée",
- "ai.continue": "Continuer",
- "ai.custom_rewrite": "Réécriture personnalisée",
- "ai.done": "AI: texte traité",
- "ai.edit": "Éditer",
- "ai.error_invalid_key": "clé API invalide",
- "ai.explain": "Expliquer",
- "ai.fix_spelling": "Corriger l'orthographe",
- "ai.frontmatter": "Générer frontmatter",
- "ai.generate": "Générer",
- "ai.hint_add_details": "Ajoute des détails",
- "ai.hint_edit_selection": "Modifie le texte sélectionné",
- "ai.hint_generate": "Génère du contenu à partir de la sélection",
- "ai.hint_improve": "Améliore la qualité du texte",
- "ai.hint_translate": "Traduit le texte sélectionné",
- "ai.improve": "Améliorer",
- "ai.inline_completion_title": "AI Inline Completion (Ctrl+J) — Complète le texte automatiquement",
- "ai.invalid_api_key": "AI: clé API invalide. Vérifiez DEEPSEEK_API_KEY dans .env",
- "ai.longer": "Allonger",
- "ai.no_api_key": "Aucune clé API configurée",
- "ai.not_configured": "⚠️ AI non configuré — ajouter DEEPSEEK_API_KEY, OPENROUTER_API_KEY ou GEMINI_API_KEY dans .env",
- "ai.processing": "⏳ AI: traitement en cours...",
- "ai.professional": "Ton professionnel",
- "ai.quota_exceeded": "AI: quota dépassé ou paiement requis",
- "ai.rewrite": "💬 Réécrire",
- "ai.rewrite_done": "AI: texte réécrit",
- "ai.rewrite_prompt": "Instruction de réécriture :",
- "ai.rewrite_title": "Réécrit le texte selon vos instructions",
- "ai.select_text": "AI: sélectionnez du texte à traiter",
- "ai.shorter": "Raccourcir",
- "ai.simplify": "Simplifier",
- "ai.summarize": "Résumer",
- "ai.text_processed": "AI: texte traité ✓",
- "ai.text_rewritten": "AI: texte réécrit",
- "ai.to_canvas": "Convertir en Canvas",
- "ai.to_list": "Convertir en liste",
- "ai.to_table": "Convertir en tableau",
- "ai.toolbar_edit": "Éditer",
- "ai.toolbar_generate": "Générer",
- "ai.toolbar_rewrite": "Réécrire",
- "ai.toolbar_tone": "Ton",
- "ai.toolbar_toolbox": "Toolbox",
- "ai.toolbar_translate": "Traduire",
- "ai.toolbox": "🧰 Boîte",
- "ai.translate_to": "Traduire en {lang}",
- "auth.cannot_delete_self": "Impossible de supprimer son propre compte",
- "auth.logged_out": "Déconnecté",
- "auth.login_button": "Se connecter",
- "auth.login_error": "Identifiants invalides",
- "auth.login_title": "Accès sécurisé à vos vaults",
- "auth.logout_title": "Déconnexion",
- "auth.network_error": "Erreur réseau — vérifiez votre connexion",
- "auth.new_password": "Nouveau mot de passe (vide = inchangé)",
- "auth.password": "Mot de passe",
- "auth.password_min": "Min. 8 caracteres",
- "auth.remember_me": "Se souvenir de moi (30 jours)",
- "auth.session_expired": "Session expirée, veuillez vous reconnecter",
- "auth.user_deleted": "Utilisateur supprimé",
- "auth.username": "Nom d'utilisateur",
- "auto.0257e77f": "🧰 Boîte ▾",
- "auto.033a9ddf": "Modèle par défaut",
- "auto.03a6e5b1": "Les opérateurs utilisés s'affichent sous forme\n de chips colorés :",
- "auto.13c3ee2c": "Recherche et autocomplétion",
- "auto.13eb7a51": "AI: clé API invalide",
- "auto.14c75ac6": "Sélectionnez les événements souhaités",
- "auto.195a8e44": "Créer répertoire",
- "auto.1a54bd69": "Exemple combiné",
- "auto.1a8a8118": "Glisser-déposer",
- "auto.1abf6661": "Le chemin affiché au-dessus du contenu est\n interactif :",
- "auto.1d3330cc": "Pas de retry automatique en cas d'échec",
- "auto.1d46301c": "Numéros de ligne",
- "auto.1efbbc9a": "Édition avec CodeMirror 6",
- "auto.209c5490": "La barre supérieure contient les éléments\n essentiels :",
- "auto.272dd29a": "Providers supportés",
- "auto.27a2f7c1": "ObsiGate supporte l'ouverture de plusieurs\n fichiers simultanément dans des onglets.",
- "auto.28bf6aab": "Sécurité : signature HMAC-SHA256",
- "auto.2a5af19b": "Le workflow se déclenche automatiquement à\n chaque événement",
- "auto.2bc24d2e": "Toutes les opérations sensibles sont tracées\n dans un journal.",
- "auto.2bc66bab": "Création de dossier via l'API",
- "auto.337d8626": "Thème clair/sombre",
- "auto.37561472": "🛡️ Sécurité",
- "auto.396d0dd8": "La barre d'outils AI apparaît sous le titre\n de l'éditeur",
- "auto.3be698f6": "Favorisez les opérateurs",
- "auto.404c8f1c": "L'autocomplétion vous aide à trouver rapidement\n :",
- "auto.41d7c6af": "Répertoire dédié",
- "auto.460b8298": "Données",
- "auto.4bc1f00a": "Utilisez des opérateurs pour affiner votre\n recherche :",
- "auto.4c908ade": "Cliquez sur un vault pour l'ouvrir et voir son\n contenu. Les dossiers affichent un chevron et un\n badge avec le nombre d'éléments.",
- "auto.4d281c6c": "AI: texte traité ✓",
- "auto.4dfa6a25": "Pas de filtrage par vault — un webhook\n écoute toutes les vaults",
- "auto.4fa5d7c2": "Édition rapide",
- "auto.559ef1da": "Workflow recommandé",
- "auto.598a0ca7": "Événements tracés",
- "auto.5a486420": "Supprimer répertoire",
- "auto.5d4a2516": "Ouvrez un fichier en mode Édition (bouton ✏️\n Éditer)",
- "auto.5d936af6": "Regex générée",
- "auto.62c3d50d": "💬 Réécrire",
- "auto.678711b5": "Sélectionnez un contexte",
- "auto.695d9e47": "Éditeur CodeMirror 6",
- "auto.6c166d35": "ObsiGate est votre porte d'entrée web vers\n vos vaults Obsidian. Accédez, naviguez et\n recherchez dans toutes vos notes depuis\n n'importe quel appareil via une interface\n moderne et responsive.",
- "auto.72e515fa": "Contrôle d'accès par vault",
- "auto.753d30a8": "Ouvrez un fichier pour le voir\n apparaître ici",
- "auto.774dbd63": "La palette de commandes permet d'accéder\n rapidement aux fichiers et aux actions sans\n utiliser la souris.",
- "auto.79f6e1dd": "Multi-fenêtres",
- "auto.7c9cdc3c": "Cochez les événements à écouter",
- "auto.7e13c97a": "Ces paramètres s'appliquent immédiatement\n côté client.",
- "auto.83a0efdf": "Réindexation",
- "auto.9122fd5c": "Configuration avancée",
- "auto.98c547ea": "ne déclenchent pas",
- "auto.a05514cb": "Création via l'API ou la palette",
- "auto.a105a2d9": "Éditer ▾",
- "auto.a131dfab": "Éditeur CodeMirror",
- "auto.a232672b": "Déploiement",
- "auto.a3319169": "📦 À propos",
- "auto.aa8ca265": "La vue utilise l'algorithme Barnes-Hut pour\n calculer la disposition en O(n log n),\n garantissant des performances fluides même avec\n des centaines de nœuds.",
- "auto.ab5e6155": "ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
- "auto.aedff644": "Créer fichier markdown",
- "auto.b114ed1c": "Ces paramètres sont sauvegardés sur le\n serveur. Certains nécessitent un redémarrage\n ou une réindexation.",
- "auto.b419c2c2": "Récepteur Python (Flask)",
- "auto.b86ba5ea": "Recherche multi-critères",
- "auto.b893650a": "Éditer fichier courant",
- "auto.ba768f01": "Événements supportés",
- "auto.c089d968": "Épinglez des fichiers pour les retrouver\n ici.",
- "auto.c14b5603": "🎨 Thèmes",
- "auto.c4922a99": "🤖 Intelligence Artificielle dans l'Éditeur",
- "auto.caac40b9": "Choisissez un thème visuel. Chaque thème\n offre un mode sombre et clair.",
- "auto.cd7332c8": "Les résultats incluent des facettes pour affiner\n :",
- "auto.ce4eb802": "Générer ▾",
- "auto.d33601dc": "Surveillance des fichiers en temps réel via\n watchdog. Les modifications sont détectées\n automatiquement et l'index est mis à jour\n sans redémarrage.",
- "auto.d4d28717": "Déclencheur",
- "auto.e0b8a606": "Définissez les patterns de tags à masquer\n dans la sidebar. Vous pouvez utiliser des\n wildcards pour cibler les tags de template.",
- "auto.e61598c1": "Partagez un document pour le voir\n apparaître ici",
- "auto.ec7260fa": "Types de fichiers supportés",
- "auto.edf89626": "L'éditeur intégré offre :",
- "auto.ef8c5f4d": "L'éditeur intégré offre une expérience complète\n d'édition :",
- "auto.f346076a": "✏️ Édition avancée",
- "auto.f5a8c116": "Événement",
- "auto.f8ba6127": "🗂️ Fichiers cachés",
- "auto.fc694490": "Suppression via l'API ou l'éditeur",
- "backup.delete": "Supprimer",
- "backup.diff": "Comparer",
- "backup.file_restored": "Fichier restauré avec succès",
- "backup.no_backups": "Aucune sauvegarde",
- "backup.preview": "Aperçu",
- "backup.purge": "Purger",
- "backup.restore": "Restaurer",
- "backup.restore_failed": "Échec de la restauration",
- "backup.select_to_restore": "Sélectionnez un backup à restaurer",
- "backup.timestamp_not_found": "Impossible de trouver le timestamp du backup",
- "backup.title": "Sauvegardes",
- "backup.version": "Version du {date}",
- "backups.delete_confirm": "Supprimer ce backup ?",
- "backups.empty": "Aucun backup disponible pour ce fichier.",
- "backups.hint": "Les backups sont crees automatiquement avant chaque modification.",
- "backups.loading": "Chargement des backups...",
- "backups.purge_file": "Supprimer TOUS les backups de",
- "backups.purge_vault": "Supprimer TOUS les backups du vault",
- "backups.restore_confirm": "a la version du backup selectionne ?\nLa version actuelle sera sauvegardee.",
- "backups.title": "Backups",
- "button.cancel": "Annuler",
- "common.actions": "Actions",
- "common.all": "Tous",
- "common.back": "Retour",
- "common.cancel": "Annuler",
- "common.close": "Fermer",
- "common.confirm": "Confirmer",
- "common.copied": "Copié !",
- "common.copy": "Copier",
- "common.create": "Créer",
- "common.delete": "Supprimer",
- "common.download": "Télécharger",
- "common.edit": "Éditer",
- "common.error": "Erreur",
- "common.loading": "Chargement...",
- "common.move": "Déplacer",
- "common.never": "Jamais",
- "common.next": "Suivant",
- "common.no": "Non",
- "common.none": "Aucun",
- "common.ok": "OK",
- "common.previous": "Précédent",
- "common.refresh": "Rafraîchir",
- "common.rename": "Renommer",
- "common.root": "(racine)",
- "common.save": "Enregistrer",
- "common.search": "Rechercher",
- "common.unknown": "Inconnue",
- "common.yes": "Oui",
- "config.about": "À propos",
- "config.about_error": "Erreur de chargement",
- "config.about_loading": "Chargement...",
- "config.add_webhook": "Ajouter",
- "config.ai_deepseek_label": "DeepSeek API Key",
- "config.ai_default_provider": "Fournisseur par défaut",
- "config.ai_gemini_label": "Gemini API Key",
- "config.ai_keys": "Clés API IA",
- "config.ai_keys_desc": "Configurez les clés API pour l'éditeur IA.",
- "config.ai_model": "Modèle",
- "config.ai_openrouter_label": "OpenRouter API Key",
- "config.api_keys_saved": "Clés API sauvegardées",
- "config.backups": "Sauvegardes",
- "config.backups_desc": "Gérez les sauvegardes automatiques de vos fichiers.",
- "config.client_config": "Configuration client",
- "config.create_share": "Créer un partage",
- "config.debounce_label": "Délai debounce (ms)",
- "config.diagnostics": "Diagnostics",
- "config.diagnostics_desc": "Informations techniques sur l'état du serveur.",
- "config.display_name": "Nom d'affichage",
- "config.error_prefix": "Erreur",
- "config.error_unknown": "Erreur inconnue",
- "config.force_reindex": "Forcer réindexation",
- "config.hidden_files": "Fichiers cachés",
- "config.hidden_files_desc": "Masquer les fichiers/dossiers commencant par un point dans l'interface (ils restent indexes et cherchables)",
- "config.hidden_files_toggle": "Masquer les fichiers/dossiers caches",
- "config.hidden_include_label": "Inclure les fichiers cachés",
- "config.hidden_whitelist_label": "Whitelist (séparés par des virgules)",
- "config.hint_activer_d_sactiver_la_surveillance_automatique_des": "Activer/désactiver la surveillance automatique des fichiers",
- "config.hint_annuler_la_recherche_apr_s_ce_d_lai_5000_120000_": "Annuler la recherche après ce délai (5000-120000)",
- "config.hint_contenu_index_par_fichier_10k_1m_r_indexation_requ": "Contenu indexé par fichier (10K-1M). Réindexation requise.",
- "config.hint_d_lai_avant_ex_cution_de_la_recherche_100_2000_": "Délai avant exécution de la recherche (100-2000)",
- "config.hint_d_lai_avant_traitement_des_changements_group_s_0_5": "Délai avant traitement des changements groupés (0.5-10 secondes)",
- "config.hint_entre_5_et_100_fichiers_sauvegarde_sur_le_serveur_": "Entre 5 et 100 fichiers (sauvegarde sur le serveur)",
- "config.hint_forcer_le_mode_polling_au_lieu_de_inotify_natif_ut": "Forcer le mode polling au lieu de inotify natif (utile si le mode natif ne fonctionne pas)",
- "config.hint_intervalle_de_scrutation_en_mode_polling_1_30_seco": "Intervalle de scrutation en mode polling (1-30 secondes)",
- "config.hint_multiplicateur_de_pertinence_pour_les_correspondan": "Multiplicateur de pertinence pour les correspondances dans le titre",
- "config.hint_nombre_de_r_sultats_affich_s_par_page_10_200_": "Nombre de résultats affichés par page (10-200)",
- "config.hint_nombre_max_de_tokens_largis_par_pr_fixe_10_200_": "Nombre max de tokens élargis par préfixe (10-200)",
- "config.hint_nombre_maximum_de_versions_backup_conservees_par_f": "Nombre maximum de versions backup conservees par fichier (1-50)",
- "config.hint_nombre_minimum_de_caract_res_avant_recherche_1_5_": "Nombre minimum de caractères avant recherche (1-5)",
- "config.hint_threads_d_di_s_la_recherche_1_8_": "Threads dédiés à la recherche (1-8)",
- "config.just_now": "a l'instant",
- "config.lang_hint": "Utilisée par l'autocomplétion et le ghost text.",
- "config.lang_label": "Langue préférée",
- "config.link_copied": "Lien copié !",
- "config.link_created": "Lien créé !",
- "config.logout": "Déconnexion",
- "config.max_backups": "Max backups par fichier",
- "config.max_backups_desc": "Nombre maximum de versions backup conservées par fichier (1-50)",
- "config.max_backups_label": "Max backups par fichier",
- "config.max_content_hint": "Contenu indexé par fichier (10K-1M). Réindexation requise.",
- "config.max_content_label": "Taille max contenu (octets)",
- "config.max_content_size": "Taille max contenu (octets)",
- "config.max_prefix_expansions": "Expansions préfixe max",
- "config.min_query_label": "Longueur min. requête",
- "config.min_query_length": "Longueur min. requête",
- "config.name_placeholder": "Votre nom",
- "config.no_keys": "Aucune clé à sauvegarder",
- "config.no_restart": "Redémarrage non requis",
- "config.note": "Note :",
- "config.open_new_window": "Ouvrir dans nouvelle fenêtre",
- "config.prefix_expansions_hint": "Nombre max de tokens élargis par préfixe (10-200)",
- "config.prefix_expansions_label": "Expansions préfixe max",
- "config.profile": "Profil utilisateur",
- "config.profile_desc": "Préférences personnelles pour personnaliser l'expérience ObsiGate.",
- "config.project_repo": "dépôt du projet",
- "config.recent_history_count": "Nombre de fichiers dans l'historique",
- "config.recent_history_count_desc": "Entre 5 et 100 fichiers (sauvegarde sur le serveur)",
- "config.reindex": "Réindexer",
- "config.reindex_desc": "Reconstruire l'index de recherche pour toutes les vaults.",
- "config.reindex_error": "Erreur de réindexation",
- "config.reindexing": "Réindexation...",
- "config.reset": "Réinitialiser",
- "config.restart_required": "Redémarrage requis",
- "config.results_per_page": "Résultats par page",
- "config.results_per_page_label": "Résultats par page",
- "config.save": "Enregistrer",
- "config.save_ai_keys": "Enregistrer les clés",
- "config.save_backend": "Sauvegarder la configuration serveur",
- "config.save_error": "Erreur de sauvegarde",
- "config.save_hidden": "Enregistrer",
- "config.saved": "✓ Sauvegardé",
- "config.saving": "Sauvegarde",
- "config.search_timeout": "Timeout recherche (ms)",
- "config.search_workers": "Workers de recherche",
- "config.search_workers_hint": "Threads dédiés à la recherche (1-8)",
- "config.search_workers_label": "Workers de recherche",
- "config.section_a-propos": "📦 À propos",
- "config.section_about": "À propos",
- "config.section_actions-sur-fichiers": "Actions sur fichiers",
- "config.section_activation": "Activation",
- "config.section_affichage": "Affichage",
- "config.section_ai": "Clés API IA",
- "config.section_arborescence-des-vaults": "Arborescence des vaults",
- "config.section_astuces-et-bonnes-pratiques": "💡 Astuces et bonnes pratiques",
- "config.section_audit-log": "Audit log",
- "config.section_authentification": "Authentification",
- "config.section_autocompletion": "Autocomplétion",
- "config.section_backend": "🖥️ Paramètres backend",
- "config.section_backup-automatique": "Backup automatique",
- "config.section_backups": "Sauvegardes",
- "config.section_barre-d-outils-ai": "Barre d'outils AI",
- "config.section_breadcrumb-et-repositionnement": "Breadcrumb et repositionnement",
- "config.section_cas-d-usage": "Cas d'usage",
- "config.section_cas-d-usage-avances": "Cas d'usage avancés",
- "config.section_chips-de-filtres-actifs": "Chips de filtres actifs",
- "config.section_cles-api-intelligence-artificielle": "🤖 Cles API Intelligence Artificielle",
- "config.section_configuration": "Configuration",
- "config.section_configuration-avancee": "Configuration avancée",
- "config.section_construit-avec": "Construit avec",
- "config.section_diag": "Diagnostics",
- "config.section_diagnostics": "Diagnostics",
- "config.section_editeur-codemirror": "Éditeur CodeMirror",
- "config.section_editeur-codemirror-6": "Éditeur CodeMirror 6",
- "config.section_edition-avancee": "✏️ Édition avancée",
- "config.section_edition-avec-codemirror-6": "Édition avec CodeMirror 6",
- "config.section_en-tete": "En-tête",
- "config.section_evenements-supportes": "Événements supportés",
- "config.section_exemples-concrets": "Exemples concrets",
- "config.section_facettes-et-tri": "Facettes et tri",
- "config.section_fichiers": "📄 Fichiers",
- "config.section_filtrage-de-la-sidebar": "Filtrage de la sidebar",
- "config.section_filtrage-de-tags": "Filtrage de tags",
- "config.section_filtrage-de-tags-template": "Filtrage de tags template",
- "config.section_fonctionnalites": "Fonctionnalités",
- "config.section_format-du-payload": "Format du payload",
- "config.section_gestion-des-onglets": "📑 Gestion des onglets",
- "config.section_hidden": "Fichiers cachés",
- "config.section_historique-recent-redemarrage-non-requis": "📋 Historique récent\n Redémarrage non requis",
- "config.section_indicateurs-visuels": "Indicateurs visuels",
- "config.section_intelligence-artificielle-dans-l-editeur": "🤖 Intelligence Artificielle dans l'Éditeur",
- "config.section_interaction": "Interaction",
- "config.section_interface": "Interface",
- "config.section_interface-utilisateur": "🧭 Interface utilisateur",
- "config.section_limitations-actuelles": "Limitations actuelles",
- "config.section_mode-commandes": "Mode commandes",
- "config.section_mode-fichiers": "Mode fichiers",
- "config.section_navigation": "Navigation",
- "config.section_navigation-entre-onglets": "Navigation entre onglets",
- "config.section_onglets": "Onglets",
- "config.section_operateurs-avances": "Opérateurs avancés",
- "config.section_optimisations": "Optimisations",
- "config.section_ouverture": "Ouverture",
- "config.section_ouverture-d-onglets": "Ouverture d'onglets",
- "config.section_palette-de-commandes": "⌨️ Palette de commandes",
- "config.section_parametres-backend-redemarrage-requis": "Paramètres backend\n Redémarrage requis",
- "config.section_parametres-de-recherche": "Paramètres de recherche",
- "config.section_partage-et-webhooks": "🔗 Partage et Webhooks",
- "config.section_partages-publics": "📤 Partages publics",
- "config.section_perf": "Performance",
- "config.section_performance": "Performance",
- "config.section_personnalisation": "🎨 Personnalisation",
- "config.section_profil-utilisateur": "👤 Profil utilisateur",
- "config.section_profile": "Profil utilisateur",
- "config.section_protection": "Protection",
- "config.section_providers-supportes": "Providers supportés",
- "config.section_publication-publique": "Publication publique",
- "config.section_raccourcis-clavier": "⌨️ Raccourcis clavier",
- "config.section_recent": "📜 Historique récent",
- "config.section_recherche": "🔍 Recherche",
- "config.section_recherche-et-autocompletion": "Recherche et autocomplétion",
- "config.section_recherche-simple": "Recherche simple",
- "config.section_redimensionnement-de-la-sidebar": "Redimensionnement de la sidebar",
- "config.section_sauvegarde-automatique": "Sauvegarde automatique",
- "config.section_sauvegardes-et-audits": "💾 Sauvegardes et Audits",
- "config.section_search": "Paramètres de recherche",
- "config.section_securite": "🛡️ Sécurité",
- "config.section_securite-signature-hmac-sha256": "Sécurité : signature HMAC-SHA256",
- "config.section_selection-de-vault": "Sélection de vault",
- "config.section_server": "Serveur",
- "config.section_shares": "Partages publics",
- "config.section_sidebar-barre-laterale": "Sidebar (barre latérale)",
- "config.section_synchronisation-automatique": "Synchronisation automatique",
- "config.section_tag-cloud": "Tag cloud",
- "config.section_tags": "🏷️ Tags",
- "config.section_tags-inline-vs-frontmatter": "Tags inline vs frontmatter",
- "config.section_theme-clair-sombre": "Thème clair/sombre",
- "config.section_themes": "🎨 Thèmes",
- "config.section_trucs-et-astuces": "Trucs et astuces",
- "config.section_types-de-fichiers-supportes": "Types de fichiers supportés",
- "config.section_visualisation": "Visualisation",
- "config.section_vue-graphe": "🗺️ Vue Graphe",
- "config.section_watcher": "Synchronisation automatique",
- "config.section_webhooks": "🪝 Webhooks",
- "config.section_workflow-recommande": "Workflow recommandé",
- "config.section_zone-de-contenu": "Zone de contenu",
- "config.server_config": "Configuration serveur",
- "config.settings_saved": "✓ Paramètres sauvegardés",
- "config.share_revoked": "Partage révoqué",
- "config.shares": "Partages publics",
- "config.shares_desc": "Gérez les liens de partage public de vos documents.",
- "config.shares_empty": "Aucun partage actif",
- "config.tag_boost_hint": "Multiplicateur de pertinence pour les correspondances dans les tags",
- "config.tag_boost_label": "Boost tags",
- "config.tag_filtering_label": "Filtrage de tags",
- "config.test": "Tester",
- "config.timeout_label": "Timeout recherche (ms)",
- "config.title": "Configuration",
- "config.title_boost": "Boost titre",
- "config.title_boost_hint": "Multiplicateur de pertinence pour les correspondances dans le titre",
- "config.title_boost_label": "Boost titre",
- "config.url_required": "URL requise",
- "config.watcher_debounce_label": "Debounce (s)",
- "config.watcher_enabled_label": "Activer la surveillance",
- "config.watcher_interval_label": "Intervalle polling (s)",
- "config.watcher_polling_label": "Mode polling (fallback)",
- "config.webhook_name_placeholder": "Nom",
- "config.webhook_url_placeholder": "URL",
- "config.webhooks": "Webhooks",
- "config.webhooks_desc": "Notifications HTTP vers des services externes lors des changements de fichiers.",
- "config.workers_label": "Workers de recherche",
- "confirm.delete_dir": "Supprimer ce dossier et tout son contenu ?",
- "confirm.delete_dir_named": "Supprimer \"{path}\" et TOUT son contenu ?",
- "confirm.delete_file": "Supprimer définitivement ce fichier ?",
- "confirm.delete_file_named": "Supprimer le fichier \"{path}\" ?",
- "confirm.logout": "Voulez-vous vraiment vous déconnecter ?",
- "conflict.no_conflicts": "Aucun conflit détecté",
- "conflict.resolve_conflict": "Garder version conflit",
- "conflict.resolve_local": "Garder version locale",
- "conflict.resolved": "Conflit résolu",
- "dashboard.all_vaults": "Tous les vaults",
- "dashboard.bookmark_add": "Ajouter aux bookmarks",
- "dashboard.bookmark_remove": "Retirer des bookmarks",
- "dashboard.bookmarks": "Favoris",
- "dashboard.conflict_date": "Conflit du ",
- "dashboard.conflict_resolved": "Conflit résolu",
- "dashboard.conflicts": "Conflits",
- "dashboard.conflicts_title": "Conflits de synchronisation",
- "dashboard.copy_btn": "Copier",
- "dashboard.count_files": "fichier",
- "dashboard.count_files_plural": "fichiers",
- "dashboard.ctrl_hover_tip": "Survolez un document avec Ctrl pour un aperçu rapide. Cliquez sur Forge dans la barre d'outils pour l'éditeur avancé.",
- "dashboard.day_ago": "il y a {n} j",
- "dashboard.error_prefix": "Erreur: ",
- "dashboard.expires": "Expire le {date}",
- "dashboard.hide": "Masquer",
- "dashboard.hour_ago": "il y a {n} h",
- "dashboard.just_now": "à l'instant",
- "dashboard.keep_conflict": "Garder le conflit",
- "dashboard.keep_original": "Garder l'original",
- "dashboard.load_error": "Erreur de chargement",
- "dashboard.loading": "Chargement...",
- "dashboard.min_ago": "il y a {n} min",
- "dashboard.no_bookmarks": "Aucun favori",
- "dashboard.no_conflicts": "Aucun conflit",
- "dashboard.no_recent": "Aucun fichier récent",
- "dashboard.no_shares": "Aucun document partagé",
- "dashboard.open_btn": "Ouvrir",
- "dashboard.pin_error": "Erreur lors de l'épinglage",
- "dashboard.pin_hint": "Épinglez des fichiers pour les retrouver ici.",
- "dashboard.quick_help": "Raccourcis & Astuces",
- "dashboard.recent": "Récents",
- "dashboard.recent_hint": "Ouvrez un fichier pour le voir apparaître ici",
- "dashboard.shared_hint": "Partagez un document pour le voir apparaître ici",
- "dashboard.shares": "Partages",
- "dashboard.show": "Afficher",
- "dashboard.size_bytes": " o",
- "dashboard.size_gb": " Go",
- "dashboard.size_kb": " Ko",
- "dashboard.size_mb": " Mo",
- "dashboard.stats": "Statistiques",
- "dashboard.stats_files": "Fichiers",
- "dashboard.stats_size": "Taille totale",
- "dashboard.stats_tags": "Tags uniques",
- "dashboard.stats_vaults": "Vaults",
- "dashboard.tab_recent": "Récents",
- "dashboard.tab_shared": "Partagés",
- "dashboard.tab_stats": "Statistiques",
- "dashboard.total_files": "Fichiers totaux",
- "dashboard.total_size": "Taille totale",
- "dashboard.total_tags": "Tags uniques",
- "dashboard.vaults_count": "Vaults",
- "dashboard.views": "{count} vue(s)",
- "dashboard.welcome": "Bienvenue dans ObsiGate",
- "dashboard.zero_files": "0 fichiers",
- "dragdrop.cross_vault_unsupported": "Cross-vault moves are not supported",
- "dragdrop.move_failed": "Move failed",
- "editor.cancel": "Annuler",
- "editor.close": "Fermer",
- "editor.delete_error": "Erreur de suppression",
- "editor.edit": "Éditer fichier courant",
- "editor.edit_current_desc": "Ouvrir le fichier actif dans l'éditeur",
- "editor.find": "Rechercher dans le fichier...",
- "editor.find_case": "Respecter la casse",
- "editor.find_regex": "Regex",
- "editor.find_whole": "Mot entier",
- "editor.forge_close_editor": "Fermer l'éditeur / modale",
- "editor.forge_help": "Aide de l'éditeur Forge",
- "editor.no_results": "Aucun résultat",
- "editor.replace": "Remplacer",
- "editor.save": "Enregistrer",
- "editor.save_error": "Erreur de sauvegarde",
- "editor.saved": "Sauvegardé ✓",
- "editor.unsaved": "Modifications non sauvegardées",
- "error.initialization": "Erreur lors de l'initialisation",
- "file.copy_path": "Copier le chemin",
- "file.delete": "Supprimer",
- "file.graph_view": "Vue graphe",
- "file.new_file": "Nouveau fichier",
- "file.new_folder": "Nouveau dossier",
- "file.properties": "Propriétés",
- "file.rename": "Renommer",
- "forge.popout": "Ouvrir dans une nouvelle fenêtre",
- "forge.popout_label": "pop-out",
- "forge.title_placeholder": "Titre...",
- "graph.back": "Retour",
- "graph.btn_directory": "📁 Dossier",
- "graph.btn_full": "🌐 Tout",
- "graph.cache_label": "(cache)",
- "graph.category": "📂 Catégorie",
- "graph.click_to_open": "cliquer pour ouvrir",
- "graph.ctrl_hover_preview": "Ctrl+survol = aperçu complet",
- "graph.ctrl_hover_preview_short": "Ctrl+survol = aperçu",
- "graph.depth": "Profondeur",
- "graph.depth_label": "prof={depth}",
- "graph.edges_count": "{count} liens",
- "graph.error_loading": "Erreur de chargement",
- "graph.export_png": "Exporter PNG",
- "graph.favorites": "Favoris",
- "graph.filter_tag": "Filtrer par tag",
- "graph.filter_type": "Filtrer par type",
- "graph.focus_mode": "Mode focus",
- "graph.forward": "Avancer",
- "graph.fullscreen": "Plein écran",
- "graph.loading": "Construction du graphe...",
- "graph.nodes_count": "{count} nœuds",
- "graph.parent": "Parent",
- "graph.published": "Publié",
- "graph.published_badge": "✅ publié",
- "graph.scope_directory": "Dossier",
- "graph.scope_full": "Vault complet",
- "graph.title": "Vue Graphique — {vault}{path}",
- "graph.type_directory": "Dossier",
- "graph.type_file": "Fichier",
- "graph.type_markdown": "Markdown",
- "header.admin_panel": "Administration",
- "header.config": "Configuration",
- "header.help": "Aide",
- "header.install_app": "Installer l'application",
- "header.logo": "ObsiGate",
- "header.logout": "Déconnexion",
- "header.menu_about": "À propos",
- "header.menu_about_desc": "Version, crédits & info",
- "header.menu_admin": "Administration",
- "header.menu_admin_desc": "Gérer les utilisateurs",
- "header.menu_config": "Configurations",
- "header.menu_config_desc": "Paramètres de l'application",
- "header.menu_help": "Guide d'utilisation",
- "header.menu_help_desc": "Ouvrir l'aide",
- "header.menu_logout": "Déconnexion",
- "header.menu_logout_desc": "Quitter la session",
- "header.menu_profile": "Profil",
- "header.menu_profile_desc": "Langue & préférences",
- "header.menu_theme": "Thème",
- "header.menu_theme_light": "Clair",
- "header.menu_toggle": "Menu",
- "header.profile": "Profil",
- "header.quick_help": "Raccourcis & Astuces",
- "header.refresh": "Rafraîchir",
- "header.search_placeholder": "Rechercher dans toutes les vaults...",
- "header.sidebar_toggle": "Afficher/Masquer la sidebar",
- "header.sync_status": "Synchronisation",
- "help.00b9572b": ":\n Résultats classés par pertinence TF-IDF",
- "help.04df1506": ":\n Recherche globale avec autocomplétion",
- "help.065f53ee": ":\n Auto-save silencieux après 2s d'inactivité,\n flash vert de confirmation",
- "help.085ec31f": ":\n Sélection rapide en haut de l'onglet Vaults",
- "help.096367af": "Indicateurs visuels",
- "help.170a6982": ": Affichage\n horizontal avec le nom du fichier et son\n icône",
- "help.229fd3e2": "Notification Discord",
- "help.2507242f": "🏷️ Tags",
- "help.29afa01d": "🪝 Webhooks",
- "help.2a2e5b0f": "⌨️ Raccourcis clavier",
- "help.2a92203f": "Trucs et astuces",
- "help.2c03a7ba": "Tags inline vs frontmatter",
- "help.2dad2bfd": "Sauvegarde automatique",
- "help.381abec7": "Limitations actuelles",
- "help.3cf370b1": "Mode commandes",
- "help.40dd7e38": "Tester avec curl",
- "help.4de7e318": ": Les 50\n dernières recherches (localStorage)",
- "help.550b0240": "Exemples concrets",
- "help.5ab8c78d": ":\n Restent ouverts jusqu'à fermeture manuelle",
- "help.5d0fbb81": ":\n Workers, boosts TF-IDF, expansions préfixe",
- "help.5f87ea79": "Tag cloud",
- "help.61ee5a22": "Interaction",
- "help.62308f96": "⌨️ Palette de commandes",
- "help.62549eca": "💾 Sauvegardes et Audits",
- "help.63f024d4": "Filtrage de tags template",
- "help.686f8313": "💡 Astuces et bonnes pratiques",
- "help.6a5ddef2": ": Les tags\n s'adaptent au vault sélectionné",
- "help.6ae5342c": ": Stats index,\n mémoire, moteur de recherche",
- "help.6b88a375": ":\n Délai debounce, résultats par page, timeout",
- "help.6b8fc728": ": ↑/↓\n pour naviguer, Enter pour sélectionner",
- "help.6d547397": "Mode fichiers",
- "help.6e940dac": ": Cliquez sur\n l'icône sidebar dans le header",
- "help.733f0559": "🗺️ Navigation",
- "help.77f4f06e": "Format du payload",
- "help.7bcbb2e3": "Redimensionnement de la sidebar",
- "help.7db70e27": ": Copier,\n Source, Télécharger, Éditer, Ouvrir dans\n nouvelle fenêtre",
- "help.83f1d9ca": "Protection",
- "help.86e3825d": "🎨 Personnalisation",
- "help.8717768b": ":\n Métadonnées du nœud (tags, liens, chemin)",
- "help.89bd3c4c": ": N'affiche\n que les nœuds avec un tag spécifique",
- "help.8c5341e0": "📑 Gestion des onglets",
- "help.8c65b2f2": "🔗 Partage et Webhooks",
- "help.8caed987": "Affichage",
- "help.9c7a0a8d": "🗺️ Vue Graphe",
- "help.9edeaa11": ":\n Contrôle la profondeur d'exploration (0-3)",
- "help.9f78b3ec": ":\n Navigation facilitée dans les fichiers longs",
- "help.a4a4e21b": ": Bouton \"×\" pour\n réinitialiser le filtre",
- "help.a579cf4d": ": Aperçu du\n contenu de la note sans naviguer",
- "help.a648c49e": "Facettes et tri",
- "help.ab2c4150": ": Fichiers (carrés)\n et dossiers (cercles) avec icônes",
- "help.about": "À propos",
- "help.access": "Accès",
- "help.action_buttons": "Boutons d'action",
- "help.actions_available": "Boutons disponibles au-dessus du contenu :",
- "help.actions_detail": "Boutons disponibles au-dessus du contenu :",
- "help.activation": "Activation",
- "help.active_filter_chips": "Chips de filtres actifs",
- "help.active_tab": "Onglet actif",
- "help.add_remove": "Ajout/Suppression",
- "help.adf30d78": "🔍 Recherche",
- "help.advanced_operators": "Opérateurs avancés",
- "help.advanced_use_cases": "Cas d'usage avancés",
- "help.ai_completes": "Complète",
- "help.ai_features": "Édition, ton, traduction, génération et outils via AI.",
- "help.ai_flash_green": "flashe en vert",
- "help.ai_tone_casual": "Ton décontracté",
- "help.all_vaults": "Tous les vaults",
- "help.atomic_writes": "Atomic writes",
- "help.audit_log": "Audit log",
- "help.auto_save": "Sauvegarde automatique",
- "help.auto_scroll": "Auto-scroll",
- "help.autocomplete": "Autocomplétion",
- "help.b6619ac9": "Ouverture",
- "help.backlinks_panel": "Panneau Backlinks",
- "help.backup_auto": "Backup automatique",
- "help.before_write": "Avant écriture",
- "help.breadcrumb": "Breadcrumb",
- "help.breadcrumb_clickable": "Breadcrumb cliquable",
- "help.breadcrumb_reposition": "Breadcrumb et repositionnement",
- "help.c017944b": "📄 Fichiers",
- "help.c48fb18a": "Authentification",
- "help.c8532869": ":\n Complétion, réécriture, traduction (voir\n section",
- "help.case_sensitive": "Case-sensitive",
- "help.cf8615b1": "Visualisation",
- "help.ci_cd": "Automatisation CI/CD",
- "help.clear": "Effacer",
- "help.clear_search": "Effacer",
- "help.click_segment": "Cliquez sur un segment",
- "help.click_tab": "Clic sur un onglet",
- "help.click_vault": "Cliquez sur le vault",
- "help.clickable": "Cliquable",
- "help.close_help": "Fermer l'aide",
- "help.close_tab_x": "Bouton ✕",
- "help.code_preview": "Code + Prévisualisation",
- "help.command_palette": "Palette de commande",
- "help.configuration": "Configuration",
- "help.content_area": "Zone de contenu",
- "help.content_shows": "La zone centrale affiche :",
- "help.context_actions": "Actions sur fichiers",
- "help.context_menu": "Menu contextuel (clic droit)",
- "help.copy_path": "Copier le chemin",
- "help.d186a19e": ":\n L'onglet \"Récent\" dans la sidebar liste vos\n derniers fichiers",
- "help.d1ea1dc6": "n8n / Make (no-code)",
- "help.d5c977ac": ": Les blocs de\n code s'adaptent au thème",
- "help.dark": "Sombre",
- "help.dashboard": "Dashboard statistiques",
- "help.delete": "Supprimer",
- "help.desc_00b9572b": ":\n Résultats classés par pertinence TF-IDF",
- "help.desc_04493800": ": 64\n caractères hexadécimaux, impossible à\n deviner",
- "help.desc_065f53ee": ":\n Auto-save silencieux après 2s d'inactivité,\n flash vert de confirmation",
- "help.desc_071da87a": "Lecture seule : La vue\n publique n'affiche que le contenu sans\n édition",
- "help.desc_0739c6a9": "Images : Support complet\n des syntaxes Obsidian",
- "help.desc_085ec31f": ":\n Sélection rapide en haut de l'onglet Vaults",
- "help.desc_0f9a1e5b": "Utilisez le filtrage sidebar\n : Plus rapide que la recherche globale pour\n trouver un fichier",
- "help.desc_11cf9e8a": "Éditer ▾ — Menu de\n modification du texte sélectionné :\n \n 🪄\n Improve writing :\n Améliore la qualité, la clarté et le\n flux du texte",
- "help.desc_13d206d4": "📊\n Convert to table :\n Transforme en tableau Markdown",
- "help.desc_13d90039": "ObsiGate permet de partager des documents via un\n lien public temporaire.",
- "help.desc_1577912b": "Rate limiting : 10\n tentatives / 15 min par IP + verrouillage\n compte",
- "help.desc_170a6982": ": Affichage\n horizontal avec le nom du fichier et son\n icône",
- "help.desc_188bc423": "Pour plus d'informations, consultez le\n dépôt du projet.",
- "help.desc_19cf5730": ": Rotation\n automatique à 10 Mo",
- "help.desc_19cfa369": "L'éditeur sauvegarde automatiquement après 2\n secondes d'inactivité. Le bouton de sauvegarde\n flashe en vert brièvement pour\n confirmer.",
- "help.desc_1a04ae18": "Masquez la sidebar : Plus\n d'espace pour le contenu",
- "help.desc_1ba39cc4": ":\n Déclencher un build quand une note de\n procédure est mise à jour",
- "help.desc_1c130d89": "— Traduit le\n texte sélectionné vers une autre langue :",
- "help.desc_1f11691c": "Atomic writes : Écriture\n via fichier temporaire + renommage atomique",
- "help.desc_2349271d": "Automatisation CI/CD :\n Déclencher un build quand une note de\n procédure est mise à jour",
- "help.desc_26f81732": "Édition rapide : Ouvrez\n l'éditeur, modifiez, sauvegardez",
- "help.desc_287be69c": ":\n Réduisez les sections Vaults/Tags selon vos\n besoins",
- "help.desc_2c220763": "Gestion des liens : Liste\n et révocation dans les Configurations",
- "help.desc_2f2c16b5": ": Publier un\n article de blog quand un fichier est marqué",
- "help.desc_31e7253b": ": Chaque backup\n est horodaté",
- "help.desc_32e3d66d": "En cas d'erreur de clé API :\n AI: clé API invalide",
- "help.desc_39979e71": ":\n Choisissez un vault pour réduire le bruit",
- "help.desc_39e97b2f": "Note : l'événement\n file_modified est réservé pour\n usage futur. Les modifications de contenu\n via l'éditeur ne déclenchent pas encore de\n webhook.",
- "help.desc_3ace259e": "— Complétion inline\n (",
- "help.desc_3c4711b8": "Événements tracés :\n Écritures, suppressions, changements de\n configuration",
- "help.desc_3d4e9189": "📋\n Simplify language :\n Simplifie le langage, évite le\n jargon",
- "help.desc_3da1896a": "Les webhooks peuvent être activés/désactivés,\n modifiés ou supprimés à tout moment. Le\n last_fired_at indique la dernière\n exécution.",
- "help.desc_3db2812f": "X-ObsiGate-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n \n Pour vérifier côté récepteur : calculez\n HMAC-SHA256(corps_brut, secret) et\n comparez. Le header\n X-ObsiGate-Event contient aussi le\n type d'événement pour un routage rapide.",
- "help.desc_40c92863": "Exploration par tags :\n Cliquez sur un tag dans une note pour voir\n toutes les notes similaires",
- "help.desc_42a81347": ": Dernière\n modification",
- "help.desc_443b76b5": "Wikilinks : Naviguez entre\n notes comme dans Obsidian",
- "help.desc_47e1e10f": "Générer ▾ — Génère du\n contenu à partir de la sélection :\n \n ℹ️ Explain this :\n Explique le texte sélectionné de\n façon claire",
- "help.desc_4de7e318": ": Les 50\n dernières recherches (localStorage)",
- "help.desc_50a33a05": "⚙️\n Generate frontmatter\n : Génère un bloc YAML frontmatter",
- "help.desc_5374685a": "Mobile-friendly : Utilisez\n le bouton hamburger sur mobile",
- "help.desc_5396ed9e": "Profitez de l'historique :\n Réutilisez vos recherches fréquentes",
- "help.desc_53bfaeca": ": Navigation par\n pages de 50 résultats",
- "help.desc_5524e921": "Ton ▾ — Change le ton du\n texte :\n \n \n 💼 Professional :\n Ton formel et professionnel",
- "help.desc_555e0305": "Pour changer de provider par défaut :\n AI_DEFAULT_PROVIDER=openrouter",
- "help.desc_56760d7f": "Chaque webhook reçoit un POST avec\n Content-Type: application/json :",
- "help.desc_58d2326f": "Navigation visuelle : Le\n breadcrumb repositionne automatiquement la\n sidebar",
- "help.desc_599f3242": ": Logger\n toutes les modifications dans une base de\n données externe",
- "help.desc_5a53de53": "📏 Make shorter :\n Rend le texte plus concis tout en\n préservant l'information",
- "help.desc_5ab8c78d": ":\n Restent ouverts jusqu'à fermeture manuelle",
- "help.desc_5d0fbb81": ":\n Workers, boosts TF-IDF, expansions préfixe",
- "help.desc_5e9a7fd9": ": Liste\n et révocation dans les Configurations",
- "help.desc_5fc28b7d": ": Créé\n automatiquement au premier démarrage",
- "help.desc_625112fd": "Sélectionnez un contexte :\n Choisissez un vault pour réduire le bruit",
- "help.desc_660051b0": "Audit externe : Logger\n toutes les modifications dans une base de\n données externe",
- "help.desc_664b1d09": "Breadcrumb cliquable :\n Repositionnez rapidement la sidebar",
- "help.desc_68c2b170": "Rotation : Rotation\n automatique à 10 Mo",
- "help.desc_6902f20d": "Favorisez les opérateurs :\n tag: et\n vault: accélèrent la recherche",
- "help.desc_698d92ec": "Gestion de l'espace :\n Réduisez les sections Vaults/Tags selon vos\n besoins",
- "help.desc_6a5ddef2": ": Les tags\n s'adaptent au vault sélectionné",
- "help.desc_6a80241e": ": La largeur\n est sauvegardée",
- "help.desc_6ae5342c": ": Stats index,\n mémoire, moteur de recherche",
- "help.desc_6b649b1e": ": Surligné en\n couleur d'accentuation",
- "help.desc_6b88a375": ":\n Délai debounce, résultats par page, timeout",
- "help.desc_6b8fc728": ": ↑/↓\n pour naviguer, Enter pour sélectionner",
- "help.desc_6bc48f59": "Créez un\n Webhook Trigger dans n8n ou\n Make",
- "help.desc_6caffeed": ": Cliquez pour\n filtrer les résultats",
- "help.desc_6d68fbc9": "from flask import Flask, request\\nimport hmac, hashlib\\n\\napp = Flask(__name__)\\nWEBHOOK_SECRET = \"mon-secret\" # même que dans ObsiGate\\n\\n@app.route(\"/webhook\", methods=[\"POST\"])\\ndef webhook():\\n # Vérifier la signature\\n signature = request.headers.get(\"X-ObsiGate-Signature\", \"\")\\n body = request.get_data()\\n expected = \"sha256=\" + hmac.new(\\n WEBHOOK_SECRET.encode(), body, hashlib.sha256\\n ).hexdigest()\\n if not hmac.compare_digest(signature, expected):\\n return \"Invalid signature\", 403\\n\\n payload = request.get_json()\\n print(f\"Événement: {payload['event']}\")\\n print(f\"Fichier: {payload['data']['vault']}/{payload['data']['path']}\")\\n return \"OK\", 200\\n\\napp.run(port=5000)\n\n n8n / Make (no-code)\n \n \n Créez un\n Webhook Trigger dans n8n ou\n Make\n \n \n Copiez l'URL générée (ex:\n https://n8n.monserveur.com/webhook/abc123)\n \n \n Dans ObsiGate, ajoutez un webhook pointant\n vers cette URL\n \n Sélectionnez les événements souhaités\n \n Le workflow se déclenche automatiquement à\n chaque événement\n \n \n\n Notification Discord\n \n Utilisez un webhook relais (n8n, Make, ou un\n script) pour transformer le payload ObsiGate en\n message Discord :",
- "help.desc_6ebebdfd": ": Chaque type\n d'opérateur a sa couleur",
- "help.desc_6f7c5889": ": Alerter un\n canal Slack/Discord quand un document est\n créé",
- "help.desc_71eeb16a": ":\n Réutilisez vos recherches fréquentes",
- "help.desc_723ed8db": "Non-root Docker : Conteneur\n tourne avec UID 1000",
- "help.desc_752789ab": ": Écriture\n via fichier temporaire + renommage atomique",
- "help.desc_759926ee": "💬 Casual : Ton\n décontracté et conversationnel",
- "help.desc_7671e56a": "Avant écriture : Sauvegarde\n du contenu original dans\n .obsigate-backup/",
- "help.desc_7708f226": "JWT + Argon2id : Tokens JWT\n avec mots de passe hachés Argon2id",
- "help.desc_7819b56f": "Copiez l'URL générée (ex:\n https://n8n.monserveur.com/webhook/abc123)",
- "help.desc_7ab5be50": "Traduire ▾ — Traduit le\n texte sélectionné vers une autre langue :\n \n 🇬🇧 English · 🇨🇳 Chinese · 🇯🇵\n Japanese · 🇩🇪 German · 🇫🇷 French ·\n 🇪🇸 Spanish",
- "help.desc_7b45490b": "Dans ObsiGate, ajoutez un webhook pointant\n vers cette URL",
- "help.desc_7e86626c": "Bouton Partager : Dans les\n actions du fichier, génère un lien unique",
- "help.desc_7f1d0512": "Path traversal : Validation\n des chemins contre les attaques\n ../",
- "help.desc_7ff2fa21": "Pendant une requête AI, un toast\n ⏳ AI: traitement en cours...\n s'affiche",
- "help.desc_803161e5": "La barre d'outils contient les boutons suivants\n :",
- "help.desc_828db5da": ": Les\n deux types sont combinés",
- "help.desc_858681a1": ":\n Durée de vie en heures",
- "help.desc_85effb89": "Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)",
- "help.desc_8717768b": ":\n Métadonnées du nœud (tags, liens, chemin)",
- "help.desc_87b348c3": "Recherche multi-critères :\n tag:docker tag:linux vault:IT\n nginx",
- "help.desc_89a8f5de": "Déploiement : Publier un\n article de blog quand un fichier est marquépublish: true",
- "help.desc_89bd3c4c": ": N'affiche\n que les nœuds avec un tag spécifique",
- "help.desc_8f0bffe3": ": Tokens JWT\n avec mots de passe hachés Argon2id",
- "help.desc_9186a3a3": "Explorez l'arborescence :\n Naviguez dans les dossiers",
- "help.desc_93902d18": "Utilisez les tags : Filtrez\n par tags pour affiner",
- "help.desc_9846fc07": ": Masque\n JWT, clés API, tokens GitHub dans les\n aperçus",
- "help.desc_985d759b": "Token unique : 64\n caractères hexadécimaux, impossible à\n deviner",
- "help.desc_991307d6": "— Génère du\n contenu à partir de la sélection :",
- "help.desc_995a711f": ": Ouvre l'éditeur\n CodeMirror",
- "help.desc_99c6f0ec": "Export : Téléchargez des\n fichiers pour backup ou partage",
- "help.desc_9cb5974c": ": Ouvrez\n plusieurs notes dans des onglets séparés",
- "help.desc_9d0e39d0": "Headers CSP :\n Content-Security-Policy, X-Frame-Options,\n XSS-Protection",
- "help.desc_9edeaa11": ":\n Contrôle la profondeur d'exploration (0-3)",
- "help.desc_9f321d1e": ":\n Écritures, suppressions, changements de\n configuration",
- "help.desc_9f78b3ec": ":\n Navigation facilitée dans les fichiers longs",
- "help.desc_9fec424d": ": Score\n TF-IDF (défaut)",
- "help.desc_a0ec890a": "— Menu de\n modification du texte sélectionné :",
- "help.desc_a13f7f2c": "Une fois terminé,\n AI: texte traité ✓ confirme\n le succès",
- "help.desc_a4a4e21b": ": Bouton \"×\" pour\n réinitialiser le filtre",
- "help.desc_a52fc3b1": ": Télécharge le\n fichier",
- "help.desc_a579cf4d": ": Aperçu du\n contenu de la note sans naviguer",
- "help.desc_a601f22e": "Allez dans ⚙️ Configurations → onglet\n Webhooks",
- "help.desc_a69804cd": "Répertoire dédié :\n .obsigate-backup/ dans le\n répertoire de la vault",
- "help.desc_a6cb3b26": "🔤\n Fix spelling & grammar\n : Corrige les fautes d'orthographe\n et de grammaire",
- "help.desc_a974f7a6": "Journal : Fichier\n data/audit.log au format JSON\n lines",
- "help.desc_a9c2c520": "Les webhooks permettent à ObsiGate de\n notifier automatiquement des services\n externes\n lorsque des fichiers ou dossiers sont modifiés.\n Chaque événement déclenche un\n POST HTTP vers l'URL de votre\n choix, avec un payload JSON détaillé.",
- "help.desc_a9db82b5": ": Centre la vue\n sur un nœud spécifique",
- "help.desc_ab2c4150": ": Fichiers (carrés)\n et dossiers (cercles) avec icônes",
- "help.desc_action_buttons": "Copier, Source, Télécharger, Éditer, Ouvrir dans nouvelle fenêtre",
- "help.desc_adv_syntax": ":\n Python, JS, HTML, CSS, Java, C++, Rust, Go\n et bien d'autres",
- "help.desc_ae213f54": ":\n Combinez opérateurs et texte libre",
- "help.desc_aec0ad2c": "💬 Réécrire — Réécrit le\n texte selon vos instructions personnalisées.\n Une boîte de dialogue vous demande votre\n instruction.",
- "help.desc_all_tags": ": Extraits des frontmatters YAML et inline (#tag)",
- "help.desc_all_vaults": ": Vue globale de tous vos contenus",
- "help.desc_autoscroll": ": La sidebar se positionne automatiquement",
- "help.desc_b54c21ef": "Contrôle d'accès par vault\n : Chaque utilisateur voit uniquement ses\n vaults",
- "help.desc_b5f89b3b": "ObsiGate intègre un assistant IA directement\n dans l'éditeur de texte. Pour l'activer,\n configurez au moins un provider AI dans le\n fichier .env (DeepSeek, OpenRouter\n ou Gemini).",
- "help.desc_b6169f2e": "Les webhooks se gèrent dans le modal\n Configurations (⚙️ menu\n Options), section Webhooks. Réservé aux\n administrateurs.",
- "help.desc_b9c94994": ": Aperçu de\n la regex appliquée",
- "help.desc_ba4578ae": "Redémarrez le conteneur :\n docker compose down && docker compose\n up -d --build",
- "help.desc_bc08f215": "Secret redactor : Masque\n JWT, clés API, tokens GitHub dans les\n aperçus",
- "help.desc_beed5686": ": Cliquez sur\n \"Thème\" dans le menu",
- "help.desc_breadcrumb": "Chemin du fichier (cliquable pour repositionner la sidebar)",
- "help.desc_c1f72521": "{\n \"event\": \"file_created\",\n \"timestamp\": \"2026-06-03T14:30:00.123456+00:00\",\n \"data\": {\n \"vault\": \"IT\",\n \"path\": \"procedures/deploiement.md\"\n }\n}\n\n Sécurité : signature HMAC-SHA256\n \n Si vous configurez un secret, chaque requête\n inclut un header\n X-ObsiGate-Signature :",
- "help.desc_c63f02cc": "— Réécrit le\n texte selon vos instructions personnalisées.\n Une boîte de dialogue vous demande votre\n instruction.",
- "help.desc_c8532869": ":\n Complétion, réécriture, traduction (voir\n section",
- "help.desc_c992f2e5": "Remplissez :\n Nom (descriptif),\n URL (endpoint cible),\n Secret (optionnel)",
- "help.desc_case_sensitive": ": Bouton \"Aa\" pour respecter la casse",
- "help.desc_cb6f0bfd": ": Ouvrez\n l'éditeur, modifiez, sauvegardez",
- "help.desc_cc8e35ba": "Notifications : Alerter un\n canal Slack/Discord quand un document est\n créé",
- "help.desc_cdf7bb46": "Recherchez intelligemment :\n Combinez opérateurs et texte libre",
- "help.desc_cf5ccd40": ": Compteurs\n par tag dans les résultats",
- "help.desc_cfe362a7": "Export PDF : Téléchargement\n du document partagé en PDF via/s/{token}/pdf",
- "help.desc_check_btn": ": Sauvegarde\n manuelle",
- "help.desc_chip_remove": ": Cliquez sur \"×\" pour retirer un filtre",
- "help.desc_chip_view": ": Voir d'un coup d'œil les filtres actifs",
- "help.desc_click_segment": ": Navigation vers ce dossier",
- "help.desc_click_tab": ": Active l'onglet",
- "help.desc_click_vault": ": Focus sur le vault dans la sidebar",
- "help.desc_close_btn": ": Ferme l'onglet (sauf s'il est seul)",
- "help.desc_cm_autocomplete": ":\n Suggestions contextuelles",
- "help.desc_cm_lines": ":\n Navigation facile",
- "help.desc_cm_search": ": Ctrl+F pour\n chercher dans le fichier",
- "help.desc_cm_syntax": ":\n Support de nombreux langages",
- "help.desc_combined": "recherche \"serveur web\" dans les fichiers\n Markdown du vault IT avec le tag linux.",
- "help.desc_context_menu": ": Clic\n droit sur un vault ou dossier → Vue\n Graphique",
- "help.desc_copy_btn": ": Copie le contenu\n source dans le presse-papiers",
- "help.desc_ctrl_tab": ": Onglet suivant",
- "help.desc_ctrl_w": ": Fermer l'onglet actif",
- "help.desc_d186a19e": ":\n L'onglet \"Récent\" dans la sidebar liste vos\n derniers fichiers",
- "help.desc_d3a71d97": "file_modified n'est pas encore\n dispatché (réservé pour version future)",
- "help.desc_d5c977ac": ": Les blocs de\n code s'adaptent au thème",
- "help.desc_d646b709": "Compte admin : Créé\n automatiquement au premier démarrage",
- "help.desc_d66e911c": ": Dans les\n actions du fichier, génère un lien unique",
- "help.desc_d915c72b": "Synchronisation : Copier\n les nouveaux fichiers vers un autre système\n (SharePoint, Google Drive)",
- "help.desc_d9cdaa89": ": Copier\n les nouveaux fichiers vers un autre système\n (SharePoint, Google Drive)",
- "help.desc_d9e8ab0b": "🧰 Boîte ▾ — Outils de\n conversion :\n \n \n 📋\n Convert to list :\n Transforme le texte en liste\n Markdown",
- "help.desc_dc4d7186": "Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte",
- "help.desc_delete_btn_new": ": Bouton\n corbeille pour supprimer",
- "help.desc_double_click": ": Ouvre le fichier dans un onglet persistant",
- "help.desc_e0525213": "✦ AI — Complétion inline\n (Ctrl+J). Complète\n automatiquement le texte là où se trouve le\n curseur.",
- "help.desc_e23c94dd": ": Dans la\n barre d'outils (le cas échéant)",
- "help.desc_e36f3eaa": "📐 Make longer :\n Ajoute des détails, exemples ou\n explications",
- "help.desc_e3fce7a3": ": Ouvre le\n fichier dans un onglet temporaire (aperçu,\n italique)",
- "help.desc_e47d2328": ": La vue\n publique n'affiche que le contenu sans\n édition",
- "help.desc_e5246734": ": Onglet\n précédent",
- "help.desc_e5b67e3d": ": Votre choix\n est sauvegardé en localStorage",
- "help.desc_e5cf90b2": "📝 Summarize :\n Résume le texte de façon concise",
- "help.desc_e6129268": "Confort de lecture :\n Ajustez la largeur de la sidebar",
- "help.desc_e7e3157a": "Timestamp : Chaque backup\n est horodaté",
- "help.desc_e93517a5": ": Téléchargement\n du document partagé en PDF via",
- "help.desc_ec5f7a26": "Expiration configurable :\n Durée de vie en heures",
- "help.desc_ecf302ae": "Multi-fenêtres : Ouvrez\n plusieurs notes dans des onglets séparés",
- "help.desc_edges": ": Wikilinks sortants\n (bleu) et backlinks entrants (vert)",
- "help.desc_eebf177f": "ObsiGate v1.7.0 - Porte\n d'entrée web pour vos vaults Obsidian",
- "help.desc_ef1531ce": ": Un\n onglet temporaire (italique) est remplacé\n par le prochain clic",
- "help.desc_ef3f8158": ": Gérez\n vos patterns de filtrage",
- "help.desc_examples": ":\n ext:sh recherche dans les scripts\n bash, ext:py dans les scripts\n Python, ext:md dans les fichiers\n Markdown.\n
",
- "help.desc_expand": ": Cliquez sur un dossier",
- "help.desc_export_png": ": Sauvegardez le\n graphe en image",
- "help.desc_f0adf67a": ": Molette pour\n zoomer, cliquer-glisser pour se déplacer",
- "help.desc_f2d4e369": "✏️\n Continue writing :\n Continue l'écriture à partir du\n texte",
- "help.desc_f53badcd": "Ajoutez votre clé API dans\n .env :\n DEEPSEEK_API_KEY=sk-...",
- "help.desc_f58da6e1": "Cliquez Ajouter",
- "help.desc_f9083ce5": ": Téléchargez des\n fichiers pour backup ou partage",
- "help.desc_f952db17": "Refresh tokens : Cookies\n HttpOnly avec rotation automatique",
- "help.desc_fa126609": "🔷\n Convert to canvas :\n Convertit en diagramme Mermaid.js",
- "help.desc_facet_vaults": ": Compteurs par vault",
- "help.desc_fc9391f3": "# Exemple avec curl + jq dans un script bash\nWEBHOOK_URL=\"https://discord.com/api/webhooks/...\"\nOBSIGATE_SECRET=\"mon-secret\"\n\n# Lire le payload depuis stdin\nread -r payload\nevent=$(echo \"$payload\" | jq -r '.event')\npath=$(echo \"$payload\" | jq -r '.data.path')\n\ncurl -H \"Content-Type: application/json\" -d \"{\n \\\"content\\\": \\\"📝 **$event** — $path\\\"\n}\" \"$WEBHOOK_URL\"\n\n Tester avec curl\n # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)\ncurl -X POST https://votre-serveur.com/webhook \\\n -H \"Content-Type: application/json\" \\\n -H \"X-ObsiGate-Event: file_created\" \\\n -H \"X-ObsiGate-Signature: sha256=$(echo -n '{\"event\":\"file_created\",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)\" \\\n -d '{\"event\":\"file_created\",\"timestamp\":\"2026-06-03T14:30:00Z\",\"data\":{\"vault\":\"IT\",\"path\":\"notes/test.md\"}}'\n\n Limitations actuelles\n \n \n Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)\n \n \n Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte\n \n \n file_modified n'est pas encore\n dispatché (réservé pour version future)\n \n Pas de retry automatique en cas d'échec\n \n Pas de filtrage par vault — un webhook\n écoute toutes les vaults\n \n \n \n \n 💾 Sauvegardes et Audits\n\n Backup automatique\n \n ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
- "help.desc_fdeaf803": ": Chaque type de\n fichier a son icône (markdown, code, etc.)",
- "help.desc_file_suggest": ": Suggestions de titres de fichiers",
- "help.desc_filter_type": ":\n Inclut/exclut fichiers, dossiers, markdown,\n autres",
- "help.desc_filter_usage": "Utilisez la barre de filtrage en haut de la sidebar pour rechercher rapidement :",
- "help.desc_filtering": "Barre de recherche pour filtrer fichiers/tags",
- "help.desc_find_in_page": ": Ctrl+F pour\n chercher dans le fichier (casse, regex, mot\n entier)",
- "help.desc_frontmatter": ": tags: [docker, linux] ou tags: docker, linux",
- "help.desc_fullscreen": ": Bouton pour\n agrandir la vue",
- "help.desc_graph_intro": "La vue graphe offre une visualisation\n interactive des connexions entre vos notes via\n les wikilinks.",
- "help.desc_graph_nav": ":\n Historique de navigation entre les nœuds",
- "help.desc_grouped_results": ": Affichage par vault avec chemins complets",
- "help.desc_hamburger": "(mobile) : Affiche/masque la sidebar",
- "help.desc_hide": "Cliquez sur l'icône sidebar dans le header",
- "help.desc_images": ": Support\n de ![[image.png]]",
- "help.desc_inline_tags": ": #docker dans le contenu markdown",
- "help.desc_instant_filter": ": Tapez pour filtrer fichiers et dossiers",
- "help.desc_logo": "Retour à l'accueil",
- "help.desc_markdown_all": ":\n Rendu complet du markdown",
- "help.desc_md_render": "Les fichiers markdown sont rendus avec :",
- "help.desc_menu_header": ": Dropdown \"Vault\" dans le menu Options",
- "help.desc_menu_options": ": Accès aux\n paramètres, thème, aide",
- "help.desc_op_ext": ": Filtrer par type de fichier",
- "help.desc_op_path": ": Filtrer par chemin",
- "help.desc_op_phrase": ": Recherche de phrase entre guillemets",
- "help.desc_op_tag": ": Filtrer par tag",
- "help.desc_op_title": ": Chercher dans les titres uniquement",
- "help.desc_op_vault": ": Rechercher dans un vault spécifique",
- "help.desc_open_file": ": Cliquez sur le nom du fichier",
- "help.desc_open_new": ": Ouvre le fichier dans un nouvel onglet",
- "help.desc_patterns": ": Ex: #<% ... %> ou #{{ ... }}",
- "help.desc_quick_select": "Dropdown pour changer de vault",
- "help.desc_rendered_content": "Markdown avec wikilinks et images",
- "help.desc_resize": "Glissez le bord droit pour ajuster la largeur",
- "help.desc_save_btn_new": ": Bouton ✓ pour\n enregistrer",
- "help.desc_search_all": ": Titre, contenu, tags, chemins",
- "help.desc_search_bar": "Recherche globale avec autocomplétion",
- "help.desc_search_intro": "\n Tapez dans la barre de recherche en haut pour\n lancer une recherche fulltext :\n
",
- "help.desc_sidebar_toggle": "Masque la sidebar pour plus d'espace",
- "help.desc_snippets": ": Extraits avec termes en surbrillance",
- "help.desc_source_btn": ": Affiche/masque le\n markdown brut",
- "help.desc_specific_vault": ": Focus sur un seul vault",
- "help.desc_syntax_hl": ":\n Coloration des blocs de code",
- "help.desc_tag_cloud_intro": "\n L'onglet Tags de la sidebar affiche un nuage de\n tags :\n
",
- "help.desc_tag_filter_use": ": Utilisez la barre de filtrage pour chercher un tag",
- "help.desc_tag_suggest": ": Suggestions de tags avec compteurs",
- "help.desc_tags_tab": "Nuage de tags cliquables",
- "help.desc_template_intro": "\n Dans les Configurations, vous pouvez masquer les\n tags de template :\n
",
- "help.desc_title_tags": "Métadonnées du fichier",
- "help.desc_trash_btn": ": Suppression du\n fichier",
- "help.desc_two_tag_types": "ObsiGate supporte deux types de tags :
",
- "help.desc_vault_context": "Changez de contexte pour filtrer l'affichage :",
- "help.desc_vault_indicator": "Affiche le contexte actif (All, vault1, etc.)",
- "help.desc_vaults_tab": "Arborescence des fichiers et dossiers",
- "help.desc_visual_search": ": Barre\n de recherche pour surligner des nœuds",
- "help.desc_wikilinks": ":\n [[lien]] et\n [[lien|texte]]",
- "help.double_click": "Double clic",
- "help.drag_drop_tabs": "Drag & drop des onglets",
- "help.e23c94dd": ": Dans la\n barre d'outils (le cas échéant)",
- "help.e3fce7a3": ": Ouvre le\n fichier dans un onglet temporaire (aperçu,\n italique)",
- "help.e5b67e3d": ": Votre choix\n est sauvegardé en localStorage",
- "help.editor_section": "Éditeur",
- "help.ef1531ce": ": Un\n onglet temporaire (italique) est remplacé\n par le prochain clic",
- "help.experience": "Expérience",
- "help.export": "Export",
- "help.external_audit": "Audit externe",
- "help.f0adf67a": ": Molette pour\n zoomer, cliquer-glisser pour se déplacer",
- "help.fallback_editor": "Éditeur de fallback",
- "help.fdeaf803": ": Chaque type de\n fichier a son icône (markdown, code, etc.)",
- "help.fe388b67": "Optimisations",
- "help.file_actions": "Actions sur le fichier ouvert :",
- "help.file_viewer": "Visionneur de fichier",
- "help.filter_toggles": "Toggles Aa, wd, .*",
- "help.filtering": "Filtrage",
- "help.find_capabilities": "Recherche regex, mot entier, case-sensitive dans le document affiché avec TreeWalker DOM.",
- "help.find_in_page": "Recherche dans la page",
- "help.footer": "© 2026 — Fait avec ☕, 🎧 et quelques",
- "help.graph_context": "Menu contextuel",
- "help.graph_depth": "Slider de profondeur",
- "help.graph_export": "Export PNG",
- "help.graph_filter_depth": "Filtres & profondeur",
- "help.graph_focus": "Mode focus",
- "help.graph_fullscreen": "Plein écran & export PNG",
- "help.graph_info": "Panneau d'info",
- "help.graph_nav_arrows": "Navigation ← → ↑",
- "help.graph_navigation": "Navigation historique ← → ↑",
- "help.graph_nodes": "Nœuds",
- "help.graph_preview": "Ctrl+survol",
- "help.graph_sticky": "Sticky panels",
- "help.graph_tag_filter": "Filtre par tag",
- "help.graph_type_filter": "Filtre par type",
- "help.graph_view": "Vue graphe",
- "help.graph_visual": "Recherche visuelle",
- "help.graph_zoom": "Zoom/Pan",
- "help.graph_zoom_pan": "Zoom & pan molette",
- "help.guide_official": "Guide officiel",
- "help.hamburger_btn": "Bouton menu hamburger",
- "help.header_section": "En-tête",
- "help.help_title": "Aide",
- "help.hide": "Masquage",
- "help.hide_help": "Masquer l'aide",
- "help.highlighted_snippets": "Snippets surlignés",
- "help.indexes": "ObsiGate indexe et affiche :",
- "help.instant": "Instantanée",
- "help.light": "Clair",
- "help.logo": "Logo ObsiGate",
- "help.markdown": "Syntaxe Markdown",
- "help.menu_header": "Menu header",
- "help.menu_options": "Menu Options",
- "help.mermaid_section": "Diagrammes Mermaid",
- "help.mermaid_templates": "Templates",
- "help.mobile": "Barre flottante résultats",
- "help.modal_title": "Guide d'utilisation ObsiGate",
- "help.multi_vault": "Multi-vault",
- "help.nav_ai": "🤖 IA Éditeur",
- "help.nav_backups": "💾 Sauvegardes",
- "help.nav_customization": "🎨 Personnalisation",
- "help.nav_editing": "✏️ Édition",
- "help.nav_files": "📁 Fichiers",
- "help.nav_graph": "🗺️ Graphe",
- "help.nav_interface": "🧭 Interface",
- "help.nav_intro": "📘 Introduction",
- "help.nav_navigation": "🗺️ Navigation",
- "help.nav_palette": "⌨️ Palette",
- "help.nav_search": "🔍 Recherche",
- "help.nav_security": "🛡️ Sécurité",
- "help.nav_sharing": "🔗 Partage",
- "help.nav_shortcuts": "⌨️ Raccourcis",
- "help.nav_tabs": "📑 Onglets",
- "help.nav_tags": "🏷️ Tags",
- "help.nav_tips": "💡 Astuces",
- "help.nav_webhooks": "🪝 Webhooks",
- "help.new_file": "Nouveau fichier",
- "help.new_folder": "Nouveau dossier",
- "help.nights": "nuits blanches 😅",
- "help.nodes_edges": "Arêtes",
- "help.other": "Autres",
- "help.outline_panel": "Panneau Plan (droite)",
- "help.pagination": "Pagination",
- "help.pal_close_the_palette": "Fermer la palette",
- "help.pal_delete_a_folder_and_its_contents": "Supprime un dossier et son contenu",
- "help.pal_delete_the_file_from_the_active_tab": "Supprime le fichier de l'onglet actif",
- "help.pal_move_desc": "Se déplacer dans la liste",
- "help.pal_open___select": "Ouvrir/sélectionner",
- "help.pal_open_the_editor_on_the_active_tab": "Ouvre l'éditeur sur l'onglet actif",
- "help.pal_open_this_guide": "Ouvre ce guide",
- "help.pal_popout_desc": "Ouvre le document dans une fenêtre détachée",
- "help.pal_toggle_light_dark": "Bascule clair/sombre",
- "help.pdf_export": "Export PDF",
- "help.persistent_display": "Affichage persistant dans la barre",
- "help.persistent_tabs": "Onglets persistants",
- "help.popout_button": "Bouton dédié",
- "help.powered_by": "Propulsé par Python, FastAPI, vanilla JS, CodeMirror, Mermaid, Lucide Icons et ❤️ pour Obsidian.",
- "help.preview_tab": "Prévisualisation",
- "help.properties": "Propriétés",
- "help.public_shares": "Publication publique",
- "help.pwa_section": "PWA",
- "help.quick_help": "Raccourcis & Astuces",
- "help.quick_select": "Sélection rapide",
- "help.rate_limiting": "Rate limiting",
- "help.recent_nav": "Navigation récente",
- "help.rename": "Renommer",
- "help.rendered_content": "Contenu rendu",
- "help.rendering_images": "Rendu d'images",
- "help.rendering_md": "Rendu dans le Markdown",
- "help.resize": "Redimensionnement",
- "help.results_counter": "Compteur de résultats",
- "help.results_grouped": "Résultats groupés",
- "help.search_bar": "Barre de recherche",
- "help.search_dropdown": "Dropdown de recherche",
- "help.search_everywhere": "Recherche dans tout",
- "help.search_facets": "Facettes",
- "help.search_filters": "Filtres de recherche",
- "help.search_history": "Historique de recherche",
- "help.search_operators": "Opérateurs de recherche",
- "help.search_placeholder": "Rechercher dans l'aide...",
- "help.search_save": "Recherches sauvegardées",
- "help.secret_redaction": "Redaction de secrets",
- "help.security_section": "Sécurité",
- "help.share_button": "Bouton Partager",
- "help.shortcut_ai_completion": "Completion IA inline (dans l'éditeur)",
- "help.shortcut_close": "Fermer l'éditeur / modale",
- "help.shortcut_close_editor": "Fermer l'éditeur / modale",
- "help.shortcut_close_palette": "Fermer la palette",
- "help.shortcut_close_tab": "Fermer l'onglet",
- "help.shortcut_command_palette": "Palette de commandes",
- "help.shortcut_file_palette": "Palette de fichiers (nav. rapide)",
- "help.shortcut_focus_search": "Focus barre de recherche",
- "help.shortcut_forge_help": "Aide de l'éditeur Forge",
- "help.shortcut_next_tab": "Onglet suivant",
- "help.shortcut_palette": "Palette de commandes",
- "help.shortcut_palette_cmd": "Palette de commandes (mode commande)",
- "help.shortcut_prev_tab": "Onglet précédent",
- "help.shortcut_quick_insert": "Insertion rapide (éditeur Forge)",
- "help.shortcut_quick_search": "Recherche rapide (hors champ texte)",
- "help.shortcut_redo": "Refaire",
- "help.shortcut_save": "Sauvegarder",
- "help.shortcut_save_file": "Sauvegarder le fichier ouvert",
- "help.shortcut_search_doc": "Rechercher dans le document",
- "help.shortcut_select": "Ouvrir/sélectionner",
- "help.shortcut_tip": "Survolez un document avec Ctrl pour un aperçu rapide. Cliquez sur Forge dans la barre d'outils pour l'éditeur avancé.",
- "help.shortcut_undo": "Annuler",
- "help.shortcuts": "Raccourcis clavier",
- "help.shortcuts_title": "Raccourcis & Astuces",
- "help.sidebar_dropdown": "Sidebar dropdown",
- "help.sidebar_features": "Fonctionnalités de la sidebar :",
- "help.sidebar_filtering": "Filtrage de la sidebar",
- "help.sidebar_section": "Sidebar (barre latérale)",
- "help.sidebar_tabs": "La sidebar est divisée en deux onglets :",
- "help.sidebar_toggle_btn": "Bouton toggle sidebar",
- "help.simple": "Simple",
- "help.simple_search": "Recherche simple",
- "help.single_click": "Simple clic",
- "help.smart_scoring": "Scoring intelligent",
- "help.sort_options": "Options de tri",
- "help.supported_languages": "Support de nombreux langages",
- "help.syncthing": "Conflits Syncthing",
- "help.syncthing_conflicts": "Conflits Syncthing",
- "help.tab_bar": "Barre d'onglets",
- "help.tab_features": "Fonctionnalités",
- "help.tab_nav": "Navigation entre onglets",
- "help.tabs_section": "Ouverture d'onglets",
- "help.theme_section": "Thème",
- "help.theme_sync": "Thème synchronisé",
- "help.theme_toggle": "Basculez entre les thèmes :",
- "help.themes": "Thèmes",
- "help.tip_hide_sidebar": "Masquez la sidebar",
- "help.tip_sidebar_filter": "Utilisez le filtrage sidebar",
- "help.tip_use_tags": "Utilisez les tags",
- "help.title": "Aide ObsiGate",
- "help.title_tags": "Titre et tags",
- "help.toc_title": "SOMMAIRE",
- "help.toolbar_section": "Barre d'outils AI",
- "help.tree_section": "Arborescence des vaults",
- "help.use_cases": "Cas d'usage",
- "help.vault_indicator": "Indicateur de vault",
- "help.vault_selector": "Sélection de vault",
- "help.vault_specific": "Vault spécifique",
- "help.webhook_blog_post": "article de blog quand un fichier est marqué",
- "help.webhook_note": "Note : l'événement",
- "help.webhook_notify": "notifier automatiquement des services",
- "help.webhook_post": "HTTP vers l'URL de votre",
- "help.webhooks": "Webhooks",
- "help.welcome_desc": "ObsiGate est votre porte d'entrée web vers vos vaults Obsidian. Accédez, naviguez et recherchez dans toutes vos notes depuis n'importe quel appareil via une interface moderne et responsive.",
- "help.welcome_tag1": "⚡ Navigation rapide",
- "help.welcome_tag2": "📁 Multi-vault",
- "help.welcome_tag3": "🏷️ Tags intelligents",
- "help.welcome_tag4": "📖 Lecture et édition",
- "help.welcome_tag5": "🔍 Recherche TF-IDF",
- "help.welcome_title": "Bienvenue dans ObsiGate",
- "help.wikilinks": "Wikilinks",
- "help.zoom_pan": "Zoom & Pan",
- "mermaid.code": "Code",
- "mermaid.copy_svg": "Copier SVG",
- "mermaid.download_png": "Télécharger PNG",
- "mermaid.preview": "Aperçu",
- "mermaid.zoom_in": "Zoom avant",
- "mermaid.zoom_out": "Zoom arrière",
- "mobile.all_vaults": "📂 Toutes les vaults",
- "modal.create_btn": "Créer",
- "modal.create_file": "Créer un fichier",
- "modal.creating": "Création...",
- "modal.delete_dir": "Supprimer le dossier",
- "modal.delete_dir_warning": "Tous les fichiers et sous-dossiers seront supprimés définitivement.",
- "modal.delete_file": "Supprimer le fichier",
- "modal.delete_irreversible": "Cette action est irréversible.",
- "modal.delete_permanently": "Supprimer définitivement",
- "modal.delete_warning": "Attention !",
- "modal.dir_to_delete": "Dossier à supprimer:",
- "modal.file_name": "Nom du fichier",
- "modal.file_to_delete": "Fichier à supprimer:",
- "modal.file_type": "Type de fichier",
- "modal.in_folder": "Dans: {path}",
- "month.april": "Avril",
- "month.august": "Août",
- "month.december": "Décembre",
- "month.february": "Février",
- "month.january": "Janvier",
- "month.july": "Juillet",
- "month.june": "Juin",
- "month.march": "Mars",
- "month.may": "Mai",
- "month.november": "Novembre",
- "month.october": "Octobre",
- "month.september": "Septembre",
- "offline.all_synced": "Toutes les modifications ont été synchronisées",
- "offline.conflict_title": "Conflit de synchronisation",
- "offline.keep_local": "Garder version locale",
- "offline.keep_server": "Garder version serveur",
- "offline.mode_offline": "Mode hors-ligne — vos modifications seront synchronisées au retour",
- "offline.pending": "{count} modification(s) en attente",
- "offline.reconnected": "Connexion rétablie — synchronisation en cours...",
- "offline.status": "Hors-ligne",
- "offline.sync_now": "Synchroniser maintenant",
- "palette.browse_dirs": "📂 Parcourir les répertoires...",
- "palette.browse_hint": "Naviguer dans les vaults pour trouver un fichier",
- "palette.browse_in": "Dans {path}",
- "palette.browse_no_results": "Aucun résultat — naviguer manuellement",
- "palette.browse_open_file": "📂 Ouvrir un fichier dans ce dossier",
- "palette.browse_root": "racine",
- "palette.close_current": "Fermer fichier courant",
- "palette.close_current_desc": "Fermer l'onglet actif sans supprimer",
- "palette.cmd_back_home": "🏠 Accueil",
- "palette.cmd_back_home_desc": "Retour à l'accueil",
- "palette.cmd_config": "⚙️ Configuration",
- "palette.cmd_config_desc": "Ouvrir les paramètres",
- "palette.cmd_create_dir": "📁 Créer répertoire",
- "palette.cmd_create_dir_desc": "Choisir la vault, naviguer, puis nommer le dossier",
- "palette.cmd_create_file": "📄 Créer fichier markdown",
- "palette.cmd_create_file_desc": "Choisir la vault, naviguer, puis nommer le fichier",
- "palette.cmd_delete_dir": "🗑️ Supprimer répertoire",
- "palette.cmd_delete_dir_desc": "Choisir la vault, naviguer et supprimer",
- "palette.cmd_theme": "🌓 Changer le thème",
- "palette.cmd_theme_desc": "Basculer clair/sombre",
- "palette.del_all_content": "Tout le contenu sera supprimé",
- "palette.delete_current": "Supprimer fichier courant",
- "palette.delete_current_desc": "Supprimer le fichier de l'onglet actif",
- "palette.delete_file_desc": "Rechercher un fichier à supprimer",
- "palette.dir_delete_confirm": "Tout le contenu sera supprimé",
- "palette.dir_name": "Nom du dossier",
- "palette.empty_dir": "📂 Dossier vide",
- "palette.error_loading": "Erreur de chargement",
- "palette.error_prefix": "Erreur : {msg}",
- "palette.file_name": "Nom du fichier",
- "palette.help_desc": "Ouvrir le guide",
- "palette.md_hint": "(.md ajouté si besoin)",
- "palette.mode_commands": "Commandes",
- "palette.mode_files": "Fichiers",
- "palette.no_results": "Aucun résultat pour \"{query}\"",
- "palette.no_vaults": "Aucune vault trouvée",
- "palette.popout_desc": "Ouvrir le document dans une fenêtre",
- "palette.reindex_desc": "Forcer la réindexation complète",
- "palette.search_commands": "Tapez une commande...",
- "palette.search_error": "Erreur de recherche",
- "palette.search_files": "Rechercher un fichier...",
- "palette.type_command": "Tapez une commande...",
- "palette.type_search": "Rechercher un fichier... (+inclure -exclure)",
- "palette.vault_count": "{count} fichiers",
- "pwa.dismiss": "Plus tard",
- "pwa.install_button": "Installer",
- "pwa.install_desc": "Installez cette application sur votre appareil pour un accès rapide.",
- "pwa.install_title": "Installer ObsiGate",
- "search.advanced_operators": "Opérateurs avancés",
- "search.aria_label": "Suggestions de recherche",
- "search.case_sensitive": "Respecter la casse",
- "search.clear": "Effacer",
- "search.dropdown_clear_history": "Effacer l'historique",
- "search.dropdown_files": "Fichiers",
- "search.dropdown_history": "Historique",
- "search.error": "Erreur de recherche",
- "search.exclude_placeholder": "Exclure: vendor/*, *.lock",
- "search.facet_tags": "Tags",
- "search.facet_vaults": "Vaults",
- "search.files_filtered": "{count} fichier(s) avec filtres",
- "search.filter_path_title": "Filtres de chemin (Alt-F)",
- "search.filters": "Filtres",
- "search.full_text": "Recherche dans tout",
- "search.highlighted_snippets": "Snippets surlignés",
- "search.include_placeholder": "Inclure: **/*.md, notes/**",
- "search.loading": "Recherche en cours...",
- "search.no_results": "Aucun résultat trouvé",
- "search.operators_help": "Opérateurs : tag:mot, vault:nom, title:mot, path:mot, ext:md",
- "search.pagination": "Pagination",
- "search.previous": "← Précédent",
- "search.query_time": "({time} ms)",
- "search.regex": "Expression régulière",
- "search.results_count": "{count} résultat(s)",
- "search.results_filtered": "{count} résultat(s) pour \"{query}\" avec filtres",
- "search.results_for": "{count} résultat(s) pour \"{query}\"",
- "search.save_search": "Sauvegarder la recherche",
- "search.saved": "Recherche sauvegardée",
- "search.sections_all_vaults": "Tous les vaults",
- "search.simple_search": "Recherche simple",
- "search.smart_scoring": "Scoring intelligent",
- "search.sort_date": "Date",
- "search.sort_relevance": "Pertinence",
- "search.sort_title": "Titre",
- "search.whole_word": "Mot entier",
- "settings.about": "📦 À propos",
- "settings.ai": "🤖 IA",
- "settings.backend": "⚙️ Backend",
- "settings.backend_hint": "Ces paramètres sont sauvegardés sur le serveur. Certains nécessitent un redémarrage ou une réindexation.",
- "settings.backend_section": "Paramètres backend",
- "settings.client_label": "Ces paramètres s'appliquent immédiatement côté client.",
- "settings.diagnostics": "🩺 Diagnostics",
- "settings.history_count_desc": "Entre 5 et 100 fichiers (sauvegarde sur le serveur)",
- "settings.history_count_label": "Nombre de fichiers dans l'historique",
- "settings.history_section": "Historique récent",
- "settings.no_restart_badge": "Redémarrage non requis",
- "settings.profile": "👤 Profil",
- "settings.reindex": "Forcer réindexation",
- "settings.restart_badge": "Redémarrage requis",
- "settings.save": "Sauvegarder",
- "settings.search": "Recherche",
- "settings.tabs": "Tabs",
- "settings.search_placeholder": "Rechercher...",
- "settings.sync": "🔄 Synchronisation",
- "settings.themes": "🎨 Thèmes",
- "share.copied": "Lien copié !",
- "share.copy_link": "Copier le lien",
- "share.create": "Créer un lien de partage",
- "share.expires": "Expire le",
- "share.never": "Jamais",
- "share.revoke": "Révoquer",
- "share.title": "Partage public",
- "shortcut.ai_completion": "Completion IA inline (dans l'éditeur)",
- "shortcut.close_editor": "Fermer l'éditeur / modale",
- "shortcut.command_palette": "Palette de commandes",
- "shortcut.editor_help": "Aide de l'éditeur Forge",
- "shortcut.file_palette": "Palette de fichiers (nav. rapide)",
- "shortcut.focus_search": "Focus barre de recherche",
- "shortcut.quick_insert": "Insertion rapide (éditeur Forge)",
- "shortcut.quick_search": "Recherche rapide (hors champ texte)",
- "shortcut.save_file": "Sauvegarder le fichier ouvert",
- "shortcut.search_in_doc": "Rechercher dans le document",
- "shortcut.tip_preview": "Survolez un document avec **Ctrl** pour un aperçu rapide. Cliquez sur **Forge** dans la barre d'outils pour l'éditeur avancé.",
- "sidebar.all_vaults": "Tous les vaults",
- "sidebar.case_sensitive": "Respecter la casse",
- "sidebar.clear_filter": "Effacer",
- "sidebar.expand_collapse": "Développer/Réduire",
- "sidebar.file_icons": "Icônes",
- "sidebar.filter_instant": "Recherche instantanée",
- "sidebar.filter_path": "Filtres de chemin",
- "sidebar.filter_placeholder": "Filtrer fichiers...",
- "sidebar.filter_results_grouped": "Résultats groupés",
- "sidebar.loading": "Chargement...",
- "sidebar.no_recent_files": "Aucun fichier récent",
- "sidebar.no_results": "Aucun repertoire ou fichier correspondant.",
- "sidebar.no_saved_searches": "Aucune recherche sauvegardée",
- "sidebar.open_file": "Ouvrir un fichier",
- "sidebar.saved_dirs_tab": "Répertoires",
- "sidebar.saved_searches_tab": "Recherches",
- "sidebar.sidebar_dropdown": "Menu contextuel vault",
- "sidebar.sidebar_filtering": "Filtrage de la sidebar",
- "sidebar.tab_backups": "Sauvegardes",
- "sidebar.tab_recent": "Récents",
- "sidebar.tab_saved": "Recherches",
- "sidebar.tab_tags": "Tags",
- "sidebar.tab_vaults": "Vaults",
- "sidebar.vault_context_all": "Toutes les vaults",
- "sidebar.vault_specific": "Vault spécifique",
- "sync.autocomplete": "Auto-complétion",
- "sync.autocomplete_active": "Auto-complétion active",
- "sync.connected": "Synchronisé",
- "sync.connected_short": "Connecté",
- "sync.disconnected": "Déconnecté",
- "sync.event_index_progress": "Vault indexé",
- "sync.event_index_start": "Démarrage index.",
- "sync.event_index_updated": "Mise à jour",
- "sync.event_vault_added": "Vault ajouté",
- "sync.event_vault_removed": "Vault supprimé",
- "sync.index_complete": "Indexation terminée ({count} fichiers)",
- "sync.index_reloaded": "Index complet rechargé",
- "sync.index_started": "Indexation démarrée ({count} vaults)",
- "sync.install_success": "ObsiGate installé avec succès !",
- "sync.model_not_loaded": "Modèle non chargé",
- "sync.no_provider": "Aucun fournisseur",
- "sync.no_recent_events": "Aucun événement récent",
- "sync.not_configured": "Non configuré",
- "sync.processing": "Traitement en cours…",
- "sync.update": "Mettre à jour",
- "sync.updating": "Mise à jour...",
- "sync.vault_added": "Vault \"{vault}\" ajouté ({count} fichiers)",
- "sync.vault_added_label": "Vault ajouté",
- "sync.vault_files": "{vault} ({files} fichiers)",
- "sync.vault_indexed_label": "Vault indexé",
- "sync.vault_removed": "Vault \"{vault}\" supprimé",
- "sync.vault_removed_label": "Vault supprimé",
- "sync.verifying": "Vérification…",
- "theme.change": "Changer le thème",
- "theme.dark": "Sombre",
- "theme.light": "Clair",
- "theme.title": "Thème",
- "themes.name_gradients": "Degrades subtils",
- "themes.name_terminal": "Noir pur & vert",
- "toast.ai_keys_saved": "Clés API sauvegardées",
- "toast.ai_no_key": "Aucune clé à sauvegarder",
- "toast.bookmark_added": "Ajouté aux favoris",
- "toast.bookmark_removed": "Retiré des favoris",
- "toast.cannot_delete_self": "Impossible de supprimer son propre compte",
- "toast.config_client_saved": "Paramètres client sauvegardés",
- "toast.config_reset": "Configuration réinitialisée",
- "toast.config_saved": "Configuration sauvegardée",
- "toast.conflict_resolved": "Conflit résolu",
- "toast.copied": "Copié !",
- "toast.copy_error": "Erreur lors de la copie",
- "toast.create_error": "Erreur lors de la création",
- "toast.created": "Créé",
- "toast.delete_error": "Erreur lors de la suppression",
- "toast.deleted": "Supprimé",
- "toast.dir_created": "Dossier \"{name}\" créé",
- "toast.dir_deleted": "Dossier supprimé ({count} fichiers)",
- "toast.dir_renamed": "Dossier renommé",
- "toast.dir_saved_search": "Répertoire sauvegardé dans les recherches",
- "toast.error_delete": "Erreur de suppression",
- "toast.error_init": "Erreur lors de l'initialisation",
- "toast.error_load": "Erreur de chargement",
- "toast.error_network": "Erreur réseau",
- "toast.error_rename": "Impossible de renommer cet élément",
- "toast.file_created": "Fichier \"{name}\" créé",
- "toast.file_deleted": "Fichier supprimé",
- "toast.file_renamed": "Fichier renommé",
- "toast.hidden_saved": "✓ Paramètres sauvegardés",
- "toast.index_reloaded": "Index complet rechargé",
- "toast.installed": "ObsiGate installé avec succès !",
- "toast.link_created": "Lien créé !",
- "toast.moved": "Déplacé",
- "toast.no_open_file": "Aucun fichier ouvert",
- "toast.not_found_tree": "Élément introuvable dans l'arborescence",
- "toast.path_copied": "Chemin copié : {path}",
- "toast.reindex_done": "Réindexation terminée",
- "toast.reindex_started": "Réindexation en cours...",
- "toast.rename_error": "Erreur lors du renommage",
- "toast.renamed": "Renommé",
- "toast.saved": "Sauvegardé",
- "toast.search_saved": "Recherche sauvegardée",
- "toast.share_revoked": "Partage révoqué",
- "toast.url_required": "URL requise",
- "toast.user_created": "Utilisateur créé",
- "toast.user_deleted": "Utilisateur supprimé",
- "toast.user_modified": "Utilisateur modifié",
- "toast.viewer_error": "Erreur lors du chargement du viewer",
- "toc.hide_panel": "Masquer le panneau",
- "toc.no_headings": "Aucun titre dans ce document",
- "toc.title": "SUR CETTE PAGE",
- "tree.new_file": "Nouveau fichier",
- "tree.new_folder": "Nouveau dossier",
- "tree.vault_home": "Accueil du vault",
- "ui.backups_versions": "Backups / Versions",
- "ui.cannot_rename": "Impossible de renommer cet élément",
- "ui.delete_name_empty": "Le nom ne peut pas etre vide",
- "ui.element_not_found": "Élément introuvable dans l'arborescence",
- "ui.vault_readonly": "Vault en lecture seule",
- "vault.add_dynamic": "Ajouter une vault",
- "vault.reindex": "Réindexer la vault",
- "vault.remove_dynamic": "Retirer la vault",
- "vaulthome.empty": "Aucun fichier dans ce repertoire",
- "vaulthome.error_loading": "Erreur de chargement:",
- "vaulthome.file_count": "{count} fichier",
- "vaulthome.file_count_plural": "{count} fichiers",
- "vaulthome.loading": "Chargement des fichiers...",
- "vaulthome.size_bytes": "o",
- "vaulthome.size_kb": "Ko",
- "vaulthome.size_mb": "Mo",
- "viewer.backlinks": "Backlinks",
- "viewer.binary_download": "Télécharger le fichier",
- "viewer.binary_unsupported": "Fichier binaire non supporté",
- "viewer.bookmark_add": "Ajouter aux bookmarks",
- "viewer.bookmark_remove": "Retirer des bookmarks",
- "viewer.bookmark_toggle": "Ajouter/Retirer des bookmarks",
- "viewer.breadcrumb_home": "Accueil",
- "viewer.click_to_download": "Cliquez pour le télécharger",
- "viewer.copied": "Copié !",
- "viewer.copy": "Copier",
- "viewer.copy_error": "Erreur lors de la copie",
- "viewer.download": "Télécharger",
- "viewer.download_md": "Télécharger en .md",
- "viewer.download_pdf": "Télécharger en PDF",
- "viewer.edit": "Éditer",
- "viewer.error": "Erreur de chargement",
- "viewer.forge_brand": "Forge",
- "viewer.forge_title": "Forge (nouvel éditeur)",
- "viewer.index_start": "Démarrage index.",
- "viewer.index_updated": "Mise à jour",
- "viewer.loaded_from_cache": "Fichier chargé depuis le cache hors-ligne",
- "viewer.loading": "Chargement du fichier...",
- "viewer.markdown": "Markdown",
- "viewer.metadata_created": "Créé",
- "viewer.metadata_modified": "Modifié",
- "viewer.metadata_path": "Chemin",
- "viewer.metadata_size": "Taille",
- "viewer.metadata_tags": "Tags",
- "viewer.metadata_vault": "Vault",
- "viewer.no_headings": "Aucun titre dans ce document",
- "viewer.on_this_page": "SUR CETTE PAGE",
- "viewer.outline": "Plan",
- "viewer.pdf_pages": "{count} pages",
- "viewer.popout": "Ouvrir dans une fenêtre",
- "viewer.raw": "Brut",
- "viewer.share": "Partager",
- "viewer.shared_doc_title": "Document partagé — cliquer pour gérer",
- "viewer.source": "Source",
- "viewer.syncing": "Mise à jour...",
- "viewer.toc": "Table des matières",
- "viewer.unsupported_file": "Ce fichier est binaire et ne peut pas être affiché",
- "webhook.trigger_create": "Création via API ou palette",
- "webhook.trigger_delete": "Suppression via API ou éditeur",
- "webhook.trigger_dir_create": "Création de dossier via API",
- "webhook.trigger_dir_delete": "Suppression de dossier via l'API",
- "webhook.trigger_dir_rename": "Renommage de dossier via l'API",
- "webhook.trigger_rename": "Renommage via l'API"
-}
diff --git a/desktop/frontend-static/manifest.json b/desktop/frontend-static/manifest.json
deleted file mode 100644
index 16513be..0000000
--- a/desktop/frontend-static/manifest.json
+++ /dev/null
@@ -1,89 +0,0 @@
-{
- "name": "ObsiGate",
- "short_name": "ObsiGate",
- "description": "Porte d'entree web pour vos vaults Obsidian - Accedez, naviguez et recherchez dans toutes vos notes",
- "start_url": "/",
- "scope": "/",
- "display": "standalone",
- "background_color": "#1a1a1a",
- "theme_color": "#2563eb",
- "orientation": "any",
- "icons": [
- {
- "src": "/static/icons/icon-72x72.svg",
- "sizes": "72x72",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-96x96.svg",
- "sizes": "96x96",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-128x128.svg",
- "sizes": "128x128",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-144x144.svg",
- "sizes": "144x144",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-152x152.svg",
- "sizes": "152x152",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-192x192.svg",
- "sizes": "192x192",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-384x384.svg",
- "sizes": "384x384",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-512x512.svg",
- "sizes": "512x512",
- "type": "image/svg+xml",
- "purpose": "any"
- },
- {
- "src": "/static/icons/icon-192x192-maskable.svg",
- "sizes": "192x192",
- "type": "image/svg+xml",
- "purpose": "maskable"
- },
- {
- "src": "/static/icons/icon-512x512-maskable.svg",
- "sizes": "512x512",
- "type": "image/svg+xml",
- "purpose": "maskable"
- }
- ],
- "categories": ["productivity", "utilities"],
- "shortcuts": [
- {
- "name": "Recherche",
- "short_name": "Recherche",
- "description": "Rechercher dans vos vaults",
- "url": "/?action=search",
- "icons": [
- {
- "src": "/static/icons/search-96x96.svg",
- "sizes": "96x96"
- }
- ]
- }
- ],
- "prefer_related_applications": false
-}
diff --git a/desktop/frontend-static/popout.html b/desktop/frontend-static/popout.html
deleted file mode 100644
index 93d4017..0000000
--- a/desktop/frontend-static/popout.html
+++ /dev/null
@@ -1,1635 +0,0 @@
-
-
-
-
-
- ObsiGate - Popout
-
-
-
-
-
-
-
-
-
-
-
-
- Chargement...
-
-
-
-
-
-
-
diff --git a/desktop/frontend-static/style.css b/desktop/frontend-static/style.css
deleted file mode 100644
index 9b915c9..0000000
--- a/desktop/frontend-static/style.css
+++ /dev/null
@@ -1,8573 +0,0 @@
-@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;
-}
-
-/* ── Editor save dot state colors ── */
-:root {
- --ok: #34d399;
- --warn: #fbbf24;
- --ai: #8b5cf6;
- --err: #f87171;
-}
-
-/* ===== THEME — DARK (fallback default, no data-theme) ===== */
-:root {
- --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;
- --resize-handle: #30363d;
- --overlay-bg: rgba(0, 0, 0, 0.5);
- --danger: #ff7b72;
- --danger-bg: #3d1a18;
- --success: #3fb950;
- --success-bg: #1a3d1f;
- --accent-card: #58a6ff;
- --accent-bg: #1f2a3a;
- --accent-text: #79c0ff;
- --green: #3fb950;
- --green-bg: #1a3d1f;
- --purple: #a371f7;
- --purple-bg: #2d1b4e;
- --surface: #161b22;
- --surface2: #0f1117;
- --surface3: #0d0f14;
- --border-md: #30363d;
- --text: #e6edf3;
- --text-2: #8b949e;
- --text-3: #484f58;
- --mono: "JetBrains Mono", monospace;
- --radius: 6px;
- --radius-lg: 10px;
-}
-
-/* ===== THEME — DARK (explicit) ===== */
-: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;
- --resize-handle: #30363d;
- --overlay-bg: rgba(0, 0, 0, 0.5);
- --danger: #ff7b72;
- --danger-bg: #3d1a18;
- --success: #3fb950;
- --success-bg: #1a3d1f;
-
- /* Accent Card variables */
- --accent-card: #58a6ff;
- --accent-bg: #1f2a3a;
- --accent-text: #79c0ff;
- --green: #3fb950;
- --green-bg: #1a3d1f;
- --purple: #a371f7;
- --purple-bg: #2d1b4e;
- --surface: #161b22;
- --surface2: #0f1117;
- --surface3: #0d0f14;
- --border-md: #30363d;
- --text: #e6edf3;
- --text-2: #8b949e;
- --text-3: #484f58;
- --mono: "JetBrains Mono", monospace;
- --radius: 6px;
- --radius-lg: 10px;
-}
-
-/* ===== 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;
- --resize-handle: #d0d7de;
- --overlay-bg: rgba(0, 0, 0, 0.3);
- --danger: #cf222e;
- --danger-bg: #ffebe9;
- --success: #1a7f37;
- --success-bg: #dafbe1;
-
- /* Accent Card variables */
- --accent-card: #4f6ef7;
- --accent-bg: #eef1fe;
- --accent-text: #2540c9;
- --green: #1a7f50;
- --green-bg: #eaf5ee;
- --purple: #6b40c4;
- --purple-bg: #f1ecfd;
- --surface: #ffffff;
- --surface2: #f2f1ef;
- --surface3: #ebe9e6;
- --border-md: rgba(0, 0, 0, 0.13);
- --text: #1a1917;
- --text-2: #6b6a67;
- --text-3: #a09e9b;
- --mono: "JetBrains Mono", monospace;
- --radius: 6px;
- --radius-lg: 10px;
-}
-
-/* ===== 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;
- justify-content: space-between;
- 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-left {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 0 1 auto;
-}
-
-.header-center {
- display: flex;
- justify-content: center;
- flex: 1 1 auto;
- max-width: 600px;
-}
-
-.header-right {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 12px;
- flex: 0 1 auto;
-}
-
-.vault-context-indicator {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- border: 1px solid var(--border);
- border-radius: 8px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.75rem;
- font-weight: 600;
- cursor: default;
- transition: all 200ms ease;
- /* Default: neutral (overridden by classes below) */
- background: var(--bg-tertiary);
- color: var(--text-muted);
-}
-.vault-context-indicator:hover {
- filter: brightness(1.15);
-}
-.vault-context-indicator i {
- opacity: 0.8;
-}
-
-/* ── Offline Indicator ── */
-.offline-indicator {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 8px;
- cursor: default;
-}
-.offline-indicator.offline i {
- color: var(--warning, #f59e0b);
- opacity: 1;
-}
-.offline-pending {
- position: absolute;
- top: -4px;
- right: -8px;
- background: var(--danger, #ef4444);
- color: #fff;
- font-size: 0.6rem;
- font-weight: 700;
- min-width: 14px;
- height: 14px;
- line-height: 14px;
- text-align: center;
- border-radius: 7px;
- padding: 0 3px;
-}
-
-/* ── Offline Search Result Badge ── */
-.search-result-offline-badge {
- display: inline-block;
- background: var(--warning, #f59e0b);
- color: #000;
- font-size: 0.55rem;
- font-weight: 700;
- padding: 1px 5px;
- border-radius: 3px;
- margin-left: 6px;
- vertical-align: middle;
- text-transform: uppercase;
-}
-
-/* ── Conflict Resolution Modal ── */
-.conflict-modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.6);
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-.conflict-modal {
- background: var(--bg-card, #1e1e2e);
- border: 1px solid var(--border, #333);
- border-radius: 8px;
- padding: 24px;
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- overflow-y: auto;
-}
-.conflict-modal h3 {
- margin: 0 0 8px;
- font-size: 1.1rem;
-}
-.conflict-modal p {
- color: var(--text-muted, #888);
- font-size: 0.85rem;
- margin: 0 0 16px;
-}
-.conflict-modal-actions {
- display: flex;
- gap: 8px;
- justify-content: flex-end;
-}
-.conflict-modal-actions button {
- padding: 8px 16px;
- border-radius: 6px;
- border: 1px solid var(--border, #333);
- background: var(--bg-input, #2a2a3c);
- color: var(--text, #ddd);
- cursor: pointer;
- font-size: 0.85rem;
-}
-.conflict-modal-actions .btn-primary {
- background: var(--accent, #3b82f6);
- border-color: var(--accent);
- color: #fff;
-}
-
-/* All vaults — blue */
-.vault-ctx-all {
- background: color-mix(in srgb, #3b82f6 14%, transparent) !important;
- border-color: color-mix(in srgb, #3b82f6 30%, var(--border)) !important;
- color: #60a5fa !important;
-}
-.vault-ctx-all i { color: #60a5fa !important; }
-
-/* Specific vault — mauve/violet */
-.vault-ctx-vault {
- background: color-mix(in srgb, #a855f7 14%, transparent) !important;
- border-color: color-mix(in srgb, #a855f7 30%, var(--border)) !important;
- color: #c084fc !important;
-}
-.vault-ctx-vault i { color: #c084fc !important; }
-
-/* Directory — amber/yellow */
-.vault-ctx-dir {
- background: color-mix(in srgb, #f59e0b 14%, transparent) !important;
- border-color: color-mix(in srgb, #f59e0b 30%, var(--border)) !important;
- color: #fbbf24 !important;
-}
-.vault-ctx-dir i { color: #fbbf24 !important; }
-
-.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);
-}
-
-.sidebar-toggle-btn {
- display: flex;
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- padding: 4px;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: color 200ms ease;
-}
-.sidebar-toggle-btn:hover {
- color: var(--accent);
-}
-.sidebar-toggle-btn.active {
- color: var(--accent);
-}
-
-.header-logo {
- font-family: "JetBrains Mono", monospace;
- font-weight: 700;
- font-size: 1.15rem;
- color: var(--accent);
- white-space: nowrap;
- display: flex;
- align-items: center;
- gap: 8px;
- cursor: pointer;
- transition: opacity 200ms ease;
- text-decoration: none;
-}
-.header-logo:hover {
- opacity: 0.8;
- text-decoration: none;
-}
-
-.search-wrapper {
- width: 100%;
- max-width: 620px;
- position: relative;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.search-wrapper .search-icon {
- position: absolute;
- left: 10px;
- top: 14px;
- color: var(--text-secondary);
- pointer-events: none;
- z-index: 1;
-}
-
-.search-input-wrapper {
- position: relative;
- flex: 1;
- display: flex;
- align-items: center;
-}
-
-.search-input-wrapper input {
- flex: 1;
- min-width: 0;
- padding: 8px 14px 8px 38px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.9rem;
- outline: none;
- transition:
- border-color 200ms ease,
- background 200ms ease;
-}
-
-.search-input-wrapper input:focus {
- border-color: var(--accent);
-}
-
-.search-actions {
- display: flex;
- align-items: center;
- gap: 2px;
- flex-shrink: 0;
- padding-left: 4px;
-}
-
-.search-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 26px;
- height: 26px;
- padding: 0 4px;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--bg-secondary);
- color: var(--text-secondary);
- cursor: pointer;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.7rem;
- font-weight: 600;
- transition: all 150ms ease;
- flex-shrink: 0;
-}
-
-.search-btn:hover {
- border-color: var(--accent);
- color: var(--accent);
- background: color-mix(in srgb, var(--accent) 10%, var(--bg-secondary));
-}
-
-.search-btn.active {
- border-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 20%, var(--bg-secondary));
- color: var(--accent);
-}
-
-.header-menu {
- position: relative;
-}
-
-.header-menu-btn {
- background: transparent;
- border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
- border-radius: 12px;
- padding: 10px 12px;
- cursor: pointer;
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- justify-content: center;
- transition:
- color 200ms ease,
- border-color 200ms ease,
- background 200ms ease,
- transform 180ms ease;
-}
-.header-menu-btn:hover {
- color: var(--accent);
- border-color: var(--accent);
- transform: translateY(-1px);
-}
-.header-menu-btn.active {
- color: var(--accent);
- border-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 10%, transparent);
-}
-
-.header-menu-dropdown {
- display: none;
- position: absolute;
- top: calc(100% + 8px);
- right: 0;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 16px;
- box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
- min-width: 260px;
- z-index: 100;
- padding: 8px;
-}
-.header-menu-dropdown.active {
- display: block;
-}
-
-.menu-list {
- display: flex;
- flex-direction: column;
-}
-
-.menu-list-row {
- width: 100%;
- display: grid;
- grid-template-columns: 28px minmax(0, 1fr);
- align-items: center;
- gap: 8px;
- padding: 4px 8px;
- border-radius: 6px;
- text-align: left;
-}
-
-.menu-list-button {
- border: none;
- background: transparent;
- cursor: pointer;
- transition:
- background 160ms ease,
- color 160ms ease;
-}
-
-.menu-list-button:hover,
-.menu-list-select-row:hover {
- background: var(--bg-hover);
-}
-
-.menu-list-button + .menu-list-button,
-.menu-list-select-row + .menu-list-button {
- margin-top: 2px;
-}
-
-.menu-list-icon {
- width: 18px;
- height: 18px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- color: var(--accent);
-}
-
-.menu-list-content {
- display: flex;
- flex-direction: column;
- gap: 1px;
- min-width: 0;
-}
-
-.menu-list-title {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.menu-list-subtitle {
- font-size: 0.7rem;
- color: var(--text-muted);
-}
-
-.menu-select {
- width: 100%;
- padding: 4px 22px 4px 0;
- border: none;
- border-radius: 0;
- background: inherit;
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.78rem;
- font-weight: 500;
- outline: none;
- cursor: pointer;
- transition: color 160ms ease;
- color-scheme: dark;
- background-color: transparent;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.menu-select option {
- background-color: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-[data-theme="dark"] .menu-select,
-[data-theme="dark"] .menu-select option {
- background-color: #1e1e1e;
- color: #e0e0e0;
-}
-.menu-select:hover,
-.menu-select:focus {
- color: var(--accent);
-}
-
-.menu-list-select {
- margin-top: 1px;
-}
-
-/* Force dark mode on all select elements */
-select {
- color-scheme: dark;
-}
-
-[data-theme="dark"] select,
-[data-theme="dark"] select option {
- background-color: #161b22 !important;
- color: #e6edf3 !important;
-}
-
-[data-theme="light"] select,
-[data-theme="light"] select option {
- background-color: #f6f8fa !important;
- color: #1f2328 !important;
-}
-
-/* Custom Dropdown Styles */
-.custom-dropdown {
- position: relative;
- width: 100%;
-}
-
-.custom-dropdown-trigger {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- padding: 8px 12px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 8px;
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- cursor: pointer;
- transition:
- border-color 200ms ease,
- background 200ms ease;
-}
-
-.custom-dropdown-trigger:hover {
- border-color: var(--accent);
- background: var(--bg-hover);
-}
-
-.custom-dropdown-trigger[aria-expanded="true"] {
- border-color: var(--accent);
-}
-
-.custom-dropdown-selected {
- flex: 1;
- text-align: left;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.custom-dropdown-menu {
- display: none;
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 6px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
- max-height: 240px;
- overflow-y: auto;
- z-index: 1000;
- list-style: none;
- padding: 3px;
- margin: 0;
-}
-
-.custom-dropdown.open .custom-dropdown-menu {
- display: block;
-}
-
-.custom-dropdown-option {
- padding: 6px 10px;
- border-radius: 4px;
- cursor: pointer;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- color: var(--text-primary);
- transition:
- background 150ms ease,
- color 150ms ease;
-}
-
-.custom-dropdown-option:hover,
-.custom-dropdown-option:focus {
- background: var(--bg-hover);
- color: var(--accent);
-}
-
-.custom-dropdown-option.selected {
- background: color-mix(in srgb, var(--accent) 15%, transparent);
- color: var(--accent);
-}
-
-/* Sidebar dropdown specific */
-.sidebar-dropdown {
- padding: 0 16px 12px;
- position: relative;
-}
-
-.sidebar-dropdown .custom-dropdown-menu {
- position: fixed;
- z-index: 10000;
- left: auto;
- right: auto;
- width: auto;
- min-width: 200px;
-}
-
-.sidebar-dropdown .custom-dropdown-trigger {
- background: var(--bg-sidebar);
-}
-
-/* Menu list dropdown specific */
-.menu-list-select-row .custom-dropdown {
- margin-top: 4px;
-}
-
-.menu-list-select-row .custom-dropdown-trigger {
- padding: 6px 10px;
- font-size: 0.78rem;
- background: var(--bg-primary);
-}
-
-.main-body {
- display: flex;
- flex: 1;
- 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 {
- width: 280px;
- min-width: 200px;
- max-width: 500px;
- background: var(--bg-sidebar);
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- transition:
- background 200ms ease,
- transform 300ms ease;
- flex-shrink: 0;
-}
-
-.sidebar.hidden {
- transform: translateX(-100%);
- width: 0;
- min-width: 0;
- border-right: none;
-}
-
-/* --- Sidebar filter --- */
-.sidebar-filter {
- padding: 10px 12px;
- border-bottom: 1px solid var(--border);
- position: relative;
- flex-shrink: 0;
- display: flex;
- align-items: center;
-}
-
-.sidebar-filter-input-wrapper {
- position: relative;
- flex: 1;
- display: flex;
- align-items: center;
-}
-
-.sidebar-filter-input-wrapper 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-wrapper input:focus {
- border-color: var(--accent);
-}
-
-.sidebar-filter-icon {
- position: absolute;
- left: 10px;
- top: 50%;
- transform: translateY(-50%);
- color: var(--text-muted);
- pointer-events: none;
- z-index: 1;
-}
-
-.sidebar-filter-actions {
- position: absolute;
- right: 4px;
- top: 50%;
- transform: translateY(-50%);
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.sidebar-filter-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--bg-secondary);
- color: var(--text-secondary);
- cursor: pointer;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.65rem;
- font-weight: 600;
- transition: all 150ms ease;
- padding: 0;
- flex-shrink: 0;
-}
-
-.sidebar-filter-btn:hover {
- border-color: var(--accent);
- color: var(--accent);
- background: color-mix(in srgb, var(--accent) 10%, var(--bg-secondary));
-}
-
-.sidebar-filter-btn.active {
- border-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 20%, var(--bg-secondary));
- color: var(--accent);
-}
-
-/* --- Sidebar tabs --- */
-.sidebar-tabs {
- display: flex;
- flex-shrink: 0;
- border-bottom: 1px solid var(--border);
- padding: 0 8px;
- gap: 2px;
- background: var(--bg-sidebar);
-}
-
-.sidebar-tab {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 8px 4px 6px;
- border: none;
- border-bottom: 2px solid transparent;
- background: transparent;
- color: var(--text-muted);
- cursor: pointer;
- transition: color 150ms ease, border-color 150ms ease;
- margin-bottom: -1px;
-}
-
-.sidebar-tab:hover {
- color: var(--text-primary);
-}
-
-.sidebar-tab.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
-}
-
-.sidebar-tab .icon {
- flex-shrink: 0;
- color: inherit;
-}
-
-/* --- Sidebar tab panels --- */
-.sidebar-tab-panel {
- display: none;
- flex: 1 1 auto;
- min-height: 0;
- overflow-y: auto;
- scroll-behavior: smooth;
- flex-direction: column;
-}
-
-.sidebar-tab-panel.active {
- display: flex;
- flex-direction: column;
-}
-
-.sidebar-tab-panel::-webkit-scrollbar {
- width: 6px;
-}
-.sidebar-tab-panel::-webkit-scrollbar-thumb {
- background: var(--scrollbar);
- border-radius: 3px;
-}
-
-#sidebar-panel-vaults .sidebar-dropdown {
- padding: 10px 12px 6px;
-}
-
-.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;
- min-width: 0; /* Allow flex item to shrink */
-}
-
-.tree-item > .tree-item-text {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0;
-}
-.tree-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-.tree-item > .badge-small {
- margin-left: 4px;
-}
-.tree-item.active {
- background: var(--bg-hover);
- color: var(--accent);
-}
-.tree-item.focused {
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- color: var(--text-primary);
- box-shadow: inset 2px 0 0 var(--accent);
-}
-.tree-item.path-selected {
- background: color-mix(in srgb, var(--accent) 20%, transparent);
- color: var(--accent);
- box-shadow:
- inset 0 0 0 2px var(--accent),
- inset 2px 0 0 var(--accent);
- border-radius: 6px;
- animation: pathPulse 1.5s ease-out;
-}
-@keyframes pathPulse {
- 0% {
- background: color-mix(in srgb, var(--accent) 40%, transparent);
- }
- 100% {
- background: color-mix(in srgb, var(--accent) 20%, transparent);
- }
-}
-.tree-item.filtered-out {
- display: none;
-}
-
-.tree-item .icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- color: var(--accent);
-}
-.tree-item.active .icon {
- color: var(--accent);
-}
-.tree-item.vault-item .icon {
- color: var(--accent);
-}
-
-.tree-item-action-btn {
- margin-left: auto;
- width: 24px;
- height: 24px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--accent);
- cursor: pointer;
- opacity: 1;
- pointer-events: auto;
- transition:
- opacity 140ms ease,
- color 140ms ease,
- background 140ms ease,
- border-color 140ms ease;
- flex-shrink: 0;
- z-index: 1;
-}
-
-.tree-item-action-btn:hover,
-.tree-item-action-btn:focus-visible {
- background: color-mix(in srgb, var(--accent) 10%, transparent);
- border-color: color-mix(in srgb, var(--accent) 25%, var(--border));
- color: var(--accent);
- outline: none;
-}
-
-/* ---------------------------------------------------------------------------
- Drag & Drop — tree items
- --------------------------------------------------------------------------- */
-
-/* Source item being dragged */
-.tree-item.drag-source {
- opacity: 0.35;
- outline: 1px dashed var(--border);
-}
-
-/* Valid drop targets during drag — subtle pulse indicator */
-.tree-item.drag-valid-target {
- outline: 1px dashed color-mix(in srgb, var(--accent) 30%, transparent);
- outline-offset: -1px;
-}
-
-/* Active drop target (hovering over) */
-.tree-item.drag-over {
- background: color-mix(in srgb, var(--accent) 18%, transparent) !important;
- outline: 2px solid var(--accent);
- outline-offset: -2px;
- border-radius: 4px;
-}
-
-/* Vault item as drop target */
-.tree-item.vault-item.drag-over {
- background: color-mix(in srgb, var(--accent) 15%, transparent) !important;
- outline: 2px solid var(--accent);
- outline-offset: -2px;
-}
-
-.tree-children {
- padding-left: 16px;
- overflow: hidden;
-}
-
-.tree-children.collapsed {
- display: none;
-}
-.tree-children.filtered-out {
- display: none;
-}
-
-.filter-results-header {
- margin-top: 8px;
-}
-
-.filter-results-group {
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 4px 0 10px 12px;
-}
-
-.filter-result-item {
- align-items: flex-start;
- white-space: normal;
- overflow: visible;
- border-radius: 6px;
-}
-
-.filter-result-text {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
- white-space: normal;
-}
-
-.filter-result-primary {
- color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.filter-result-secondary {
- color: var(--text-muted);
- font-size: 0.75rem;
- word-break: break-all;
- opacity: 0.8;
-}
-
-.sidebar-filter-empty {
- padding: 12px 16px;
- color: var(--text-muted);
- font-size: 0.85rem;
-}
-
-.filter-highlight {
- background: color-mix(in srgb, var(--accent) 22%, transparent);
- color: var(--accent);
- padding: 0 2px;
- border-radius: 4px;
-}
-
-/* --- Tag Cloud --- */
-.tag-cloud {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- padding: 14px 16px 16px;
- align-content: flex-start;
-}
-
-.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;
-}
-.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,
- opacity 300ms ease;
- z-index: 10;
-}
-.sidebar-resize-handle:hover,
-.sidebar-resize-handle.active {
- background: var(--accent);
- opacity: 0.5;
-}
-.sidebar-resize-handle.hidden {
- width: 0;
- opacity: 0;
- pointer-events: none;
-}
-
-/* --- Content Wrapper (tabs + content) --- */
-.content-wrapper {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-width: 0;
- overflow: hidden;
-}
-
-/* --- Content Area --- */
-.content-area {
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- padding: clamp(16px, 3vw, 40px) clamp(16px, 4vw, 40px) 60px;
- transition:
- background 200ms ease,
- margin 300ms ease;
- min-width: 0;
-}
-
-/* Center content when sidebar is hidden */
-.sidebar.hidden ~ .content-wrapper .content-area {
- margin: 0 auto;
- max-width: 1200px;
-}
-.content-area::-webkit-scrollbar {
- width: 8px;
-}
-.content-area::-webkit-scrollbar-thumb {
- background: var(--scrollbar);
- border-radius: 4px;
-}
-
-/* --- Right Sidebar Resize Handle --- */
-.right-sidebar-resize-handle {
- width: 5px;
- cursor: ew-resize;
- background: transparent;
- flex-shrink: 0;
- transition:
- background 150ms ease,
- opacity 300ms ease;
- z-index: 10;
-}
-.right-sidebar-resize-handle:hover,
-.right-sidebar-resize-handle.active {
- background: var(--accent);
- opacity: 0.5;
-}
-.right-sidebar-resize-handle.hidden {
- width: 0;
- opacity: 0;
- pointer-events: none;
-}
-
-/* --- Right Sidebar (Outline/TOC) --- */
-.right-sidebar {
- width: 280px;
- min-width: 200px;
- max-width: 400px;
- background: var(--bg-sidebar);
- border-left: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- transition:
- background 200ms ease,
- transform 300ms ease,
- width 300ms ease;
- flex-shrink: 0;
-}
-
-.right-sidebar.hidden {
- transform: translateX(100%);
- width: 0;
- min-width: 0;
- border-left: none;
-}
-
-/* Right sidebar header */
-.right-sidebar-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-sidebar);
- flex-shrink: 0;
-}
-
-.right-sidebar-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: 0;
-}
-
-.right-sidebar-toggle-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- background: transparent;
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 150ms ease;
-}
-
-.right-sidebar-toggle-btn:hover {
- background: var(--bg-hover);
- border-color: var(--accent);
- color: var(--accent);
-}
-
-.right-sidebar-toggle-btn i {
- transition: transform 200ms ease;
-}
-
-.right-sidebar.hidden .right-sidebar-toggle-btn i {
- transform: rotate(180deg);
-}
-
-/* Outline panel */
-.outline-panel {
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- padding: 8px 0;
- min-height: 0;
-}
-
-.outline-panel::-webkit-scrollbar {
- width: 6px;
-}
-
-.outline-panel::-webkit-scrollbar-thumb {
- background: var(--scrollbar);
- border-radius: 3px;
-}
-
-.outline-list {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 0 8px;
-}
-
-.outline-item {
- display: flex;
- align-items: center;
- padding: 8px 12px;
- border-radius: 6px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.82rem;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 150ms ease;
- position: relative;
- text-decoration: none;
- line-height: 1.4;
- word-break: break-word;
-}
-
-.outline-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.outline-item.active {
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- color: var(--accent);
- font-weight: 500;
-}
-
-.outline-item.active::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: var(--accent);
- border-radius: 0 2px 2px 0;
-}
-
-/* Heading levels */
-.outline-item.level-2 {
- font-weight: 600;
- font-size: 0.85rem;
- padding-left: 12px;
-}
-
-.outline-item.level-3 {
- font-weight: 400;
- font-size: 0.8rem;
- padding-left: 28px;
- color: var(--text-muted);
-}
-
-.outline-item.level-3:hover {
- color: var(--text-secondary);
-}
-
-.outline-item.level-3.active {
- color: var(--accent);
-}
-
-/* Empty state */
-.outline-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- gap: 12px;
- color: var(--text-muted);
- font-size: 0.85rem;
- text-align: center;
-}
-
-/* Reading progress */
-.reading-progress {
- padding: 12px 16px;
- border-top: 1px solid var(--border);
- background: var(--bg-sidebar);
- flex-shrink: 0;
-}
-
-.reading-progress-bar {
- width: 100%;
- height: 6px;
- background: color-mix(in srgb, var(--border) 50%, transparent);
- border-radius: 3px;
- overflow: hidden;
- margin-bottom: 8px;
-}
-
-.reading-progress-fill {
- height: 100%;
- background: var(--accent);
- border-radius: 3px;
- transition: width 150ms ease;
- width: 0%;
-}
-
-.reading-progress-text {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--text-muted);
- text-align: center;
-}
-
-/* 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.75rem;
- margin-right: 6px;
- margin-bottom: 4px;
- cursor: pointer;
- transition:
- opacity 150ms ease,
- transform 100ms ease;
-}
-.file-tag:hover {
- opacity: 0.8;
- transform: translateY(-1px);
-}
-.file-actions {
- margin-top: 8px;
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
-}
-.btn-action {
- 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;
- display: inline-flex;
- align-items: center;
- gap: 4px;
-}
-.btn-action:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-.btn-action.active {
- color: var(--accent);
- border-color: var(--accent);
- background: var(--tag-bg);
-}
-
-/* ===== FRONTMATTER — ACCENT CARD ===== */
-
-/* Frontmatter section wrapper */
-.fm-section {
- margin-bottom: 20px;
-}
-
-/* Header toggle */
-.fm-header {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.78rem;
- color: var(--text-muted);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 12px;
- user-select: none;
- transition: color 150ms ease;
-}
-.fm-header:hover {
- color: var(--text-secondary);
-}
-
-/* Chevron icon */
-.fm-chevron {
- transition: transform 0.2s;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-.fm-chevron.open {
- transform: rotate(0deg);
-}
-.fm-chevron.closed {
- transform: rotate(-90deg);
-}
-
-/* Accent Card main container */
-.ac-card {
- border: 1px solid var(--border-md);
- border-left: 3px solid var(--accent-card);
- border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
- background: var(--surface);
- overflow: hidden;
-}
-
-/* ZONE 1 — Top strip */
-.ac-top {
- padding: 12px 16px 10px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
-}
-.ac-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
- font-family: var(--mono);
- letter-spacing: -0.01em;
- flex-shrink: 0;
-}
-.ac-badge {
- padding: 2px 9px;
- border-radius: 99px;
- font-size: 11px;
- font-weight: 500;
- display: inline-flex;
- align-items: center;
- gap: 4px;
-}
-.ac-badge.green {
- background: var(--green-bg);
- color: var(--green);
-}
-.ac-badge.blue {
- background: var(--accent-bg);
- color: var(--accent-text);
-}
-.ac-badge.purple {
- background: var(--purple-bg);
- color: var(--purple);
-}
-.ac-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: currentColor;
- opacity: 0.7;
-}
-
-/* ZONE 2 — Body 2 columns */
-.ac-body {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0;
-}
-.ac-col {
- padding: 12px 16px;
-}
-.ac-col + .ac-col {
- border-left: 1px solid var(--border);
-}
-.ac-row {
- display: flex;
- align-items: baseline;
- gap: 6px;
- padding: 4px 0;
- font-size: 12.5px;
-}
-.ac-row + .ac-row {
- border-top: 1px solid var(--border);
-}
-.ac-k {
- color: var(--text-3);
- font-size: 11px;
- font-family: var(--mono);
- min-width: 110px;
- flex-shrink: 0;
-}
-.ac-v {
- color: var(--text);
- font-weight: 500;
- font-size: 12.5px;
-}
-.ac-v.mono {
- font-family: var(--mono);
- font-weight: 400;
- color: var(--text-2);
- font-size: 11px;
-}
-.ac-v.muted {
- color: var(--text-3);
- font-weight: 400;
-}
-
-/* ZONE 3 — Tags row */
-.ac-tags-row {
- padding: 10px 16px;
- border-top: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 10px;
-}
-.ac-tags-k {
- font-size: 11px;
- font-family: var(--mono);
- color: var(--text-3);
- flex-shrink: 0;
-}
-.ac-tags-wrap {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
-}
-.ac-tag {
- padding: 2px 9px;
- border-radius: 99px;
- font-size: 11px;
- font-weight: 500;
- background: var(--accent-bg);
- color: var(--accent-text);
- border: 1px solid rgba(79, 110, 247, 0.18);
-}
-
-/* ZONE 4 — Flags row */
-.ac-flags-row {
- padding: 9px 16px;
- border-top: 1px solid var(--border);
- background: var(--surface2);
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
-}
-.ac-flags-k {
- font-size: 11px;
- font-family: var(--mono);
- color: var(--text-3);
- margin-right: 2px;
-}
-.flag-chip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 8px;
- border-radius: var(--radius);
- font-size: 11px;
- font-weight: 500;
- border: 1px solid var(--border-md);
-}
-.flag-chip.on {
- background: var(--green-bg);
- color: var(--green);
- border-color: rgba(26, 127, 80, 0.2);
-}
-.flag-chip.off {
- background: var(--surface3);
- color: var(--text-3);
-}
-.flag-dot {
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: currentColor;
- opacity: 0.7;
-}
-
-/* --- 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 --- */
-.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;
-}
-
-/* Image placeholders */
-.image-not-found {
- display: inline-block;
- padding: 8px 12px;
- background: var(--danger-bg);
- color: var(--danger);
- border: 1px dashed var(--danger);
- border-radius: 4px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.85rem;
- cursor: help;
-}
-
-/* Markdown images */
-.file-content img {
- max-width: 100%;
- height: auto;
- display: block;
- margin: 16px 0;
- border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.file-content a img {
- cursor: pointer;
- transition: opacity 0.2s ease;
-}
-
-.file-content a img:hover {
- opacity: 0.85;
-}
-
-/* --- 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;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
-}
-.search-results-summary-text {
- display: inline;
-}
-.search-results-active-tags {
- display: inline-flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
-}
-.search-results-active-tag {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 8px;
- background: var(--tag-bg);
- color: var(--tag-text);
- border-radius: 999px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.75rem;
- line-height: 1.2;
-}
-.search-results-active-tag-remove {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border: none;
- background: transparent;
- color: inherit;
- cursor: pointer;
- padding: 0;
-}
-.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;
- position: relative;
-}
-.search-result-item:hover {
- background: var(--bg-hover);
- border-color: var(--accent);
-}
-.search-result-ext {
- position: absolute;
- top: 8px;
- right: 10px;
- font-size: 0.65rem;
- font-weight: 700;
- text-transform: uppercase;
- color: var(--accent);
- background: var(--accent-bg, rgba(99, 102, 241, 0.12));
- padding: 2px 8px;
- border-radius: 4px;
- letter-spacing: 0.5px;
- border: 1px solid rgba(99, 102, 241, 0.2);
-}
-.search-filters-row {
- display: flex;
- gap: 4px;
- flex-wrap: wrap;
- margin-top: 4px;
-}
-.search-filter-badge {
- font-size: 0.65rem;
- font-family: "JetBrains Mono", monospace;
- padding: 1px 6px;
- border-radius: 3px;
- background: rgba(99, 102, 241, 0.1);
- color: var(--accent);
- font-weight: 600;
-}
-.search-filter-badge.path {
- background: rgba(5, 150, 105, 0.1);
- color: #059669;
-}
-
-/* Save search button */
-.search-save-btn {
- padding: 4px 10px;
- font-size: 0.75rem;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-.search-save-btn:hover { background: var(--bg-hover); }
-
-/* Saved searches sidebar */
-.saved-search-item {
- position: relative;
- padding: 10px 12px;
- cursor: pointer;
- border-radius: 6px;
- transition: background 0.15s;
- border: 1px solid transparent;
- margin-bottom: 4px;
-}
-.saved-search-item:hover { background: var(--bg-hover); border-color: var(--border); }
-.saved-search-query {
- font-size: 0.82rem;
- font-family: "JetBrains Mono", monospace;
- line-height: 1.4;
- word-break: break-word;
- overflow-wrap: anywhere;
- margin-bottom: 6px;
-}
-.saved-search-meta {
- display: flex;
- gap: 4px;
- align-items: center;
- flex-wrap: wrap;
- margin-bottom: 2px;
-}
-.saved-search-badges {
- display: flex;
- gap: 4px;
- align-items: center;
- flex-wrap: wrap;
- flex: 1;
- min-width: 0;
-}
-.saved-search-type-icon {
- margin-left: auto;
- flex-shrink: 0;
- opacity: 0.6;
-}
-/* Saved filter bar */
-.saved-filter-bar {
- display: flex;
- gap: 6px;
- padding: 8px 10px 10px;
-}
-.saved-filter-pill {
- padding: 3px 10px;
- font-size: 0.7rem;
- font-weight: 500;
- border: 1px solid var(--border);
- border-radius: 12px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- transition: background 0.15s, color 0.15s, border-color 0.15s;
-}
-.saved-filter-pill:hover { background: var(--bg-hover); color: var(--text-primary); }
-.saved-filter-pill.active {
- background: var(--accent-bg, rgba(99,102,241,0.12));
- color: var(--accent);
- border-color: var(--accent);
-}
-/* Offline indicator */
-.offline-indicator {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 2px 8px;
- border-radius: 4px;
- font-size: 0.7rem;
- cursor: default;
-}
-.offline-indicator.offline {
- background: var(--warn-bg, rgba(251,191,36,0.12));
- color: var(--warn);
- border: 1px solid var(--warn);
-}
-.offline-pending {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 16px;
- height: 16px;
- padding: 0 4px;
- border-radius: 8px;
- background: var(--warn);
- color: #000;
- font-size: 0.6rem;
- font-weight: 700;
-}
-/* Theme picker grid */
-.theme-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
- gap: 10px;
- margin-top: 12px;
-}
-.theme-card {
- background: var(--bg-secondary);
- border: 2px solid var(--border);
- border-radius: 8px;
- padding: 10px;
- cursor: pointer;
- transition: border-color 0.2s, transform 0.15s;
-}
-.theme-card:hover { border-color: var(--accent); transform: translateY(-1px); }
-.theme-card.active { border-color: var(--accent-green); }
-.theme-card-preview {
- height: 40px;
- border-radius: 4px;
- margin-bottom: 8px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
-}
-.theme-card-preview-bar {
- height: 14px;
-}
-.theme-card-preview-body {
- flex: 1;
- display: flex;
- align-items: center;
- padding: 0 6px;
-}
-.theme-card-preview-btn {
- width: 50%;
- height: 10px;
- border-radius: 3px;
-}
-.theme-card-name {
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 2px;
-}
-.theme-card-desc {
- font-size: 0.65rem;
- color: var(--text-muted);
-}
-.theme-mode-toggle {
- display: flex;
- gap: 0;
- margin-top: 16px;
- border: 1px solid var(--border);
- border-radius: 6px;
- overflow: hidden;
- width: fit-content;
-}
-.theme-mode-btn {
- padding: 4px 14px;
- font-size: 0.7rem;
- border: none;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
-}
-.theme-mode-btn.active {
- background: var(--accent);
- color: #fff;
-}
-/* Profile section */
-.profile-form { max-width: 420px; }
-.profile-field { margin-bottom: 14px; }
-.profile-field label { display: block; font-size: 0.75rem; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
-.profile-field .config-input,
-.profile-field .config-select {
- width: 100%; padding: 6px 10px; font-size: 0.8rem;
- background: var(--bg-primary); color: var(--text-primary);
- border: 1px solid var(--border); border-radius: 6px;
- font-family: inherit;
-}
-.profile-hint { display: block; font-size: 0.65rem; color: var(--text-muted); margin-top: 4px; }
-.profile-saved { font-size: 0.7rem; color: var(--accent-green); margin-left: 10px; }
-.config-save-btn {
- padding: 6px 18px; font-size: 0.75rem; font-weight: 600;
- background: var(--accent); color: #fff;
- border: 1px solid var(--accent); border-radius: 6px;
- cursor: pointer; transition: opacity 0.15s;
-}
-.config-save-btn:hover { opacity: 0.85; }
-.saved-search-vault {
- font-size: 0.65rem;
- color: var(--text-muted);
- padding: 1px 5px;
- background: var(--bg-secondary);
- border-radius: 3px;
-}
-.saved-search-filters {
- font-size: 0.65rem;
- color: var(--accent);
- display: flex;
- gap: 4px;
- flex-wrap: wrap;
-}
-.saved-search-path {
- font-size: 0.6rem;
- color: #059669;
- background: rgba(5,150,105,0.08);
- padding: 1px 5px;
- border-radius: 3px;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.saved-search-delete {
- position: absolute;
- top: 6px;
- right: 8px;
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- font-size: 0.9rem;
- padding: 2px 6px;
- opacity: 0;
- transition: opacity 0.15s;
-}
-.saved-search-item:hover .saved-search-delete { opacity: 1; }
-.saved-search-delete:hover { color: var(--text-error); }
-.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-snippet .search-highlight {
- background: color-mix(in srgb, var(--accent) 25%, transparent);
- color: var(--accent);
- font-weight: 600;
- padding: 1px 3px;
- border-radius: 3px;
-}
-.search-result-title .search-highlight {
- background: color-mix(in srgb, var(--accent) 25%, transparent);
- color: var(--accent);
- font-weight: 700;
- padding: 1px 3px;
- border-radius: 3px;
-}
-.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;
-}
-
-/* --- Loading indicator --- */
-.loading-indicator {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- color: var(--text-muted);
- gap: 12px;
-}
-.loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid var(--border);
- border-top-color: var(--accent);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-/* --- Search progress bar --- */
-.search-progress-bar {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
- z-index: 9999;
- background: transparent;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.15s;
-}
-.search-progress-bar.active {
- opacity: 1;
-}
-.search-progress-bar .search-progress-bar__fill {
- height: 100%;
- background: var(--accent);
- width: 0%;
- animation: progress-indeterminate 1.5s ease-in-out infinite;
-}
-@keyframes progress-indeterminate {
- 0% {
- width: 0%;
- margin-left: 0%;
- }
- 50% {
- width: 40%;
- margin-left: 30%;
- }
- 100% {
- width: 0%;
- margin-left: 100%;
- }
-}
-.search-time-badge {
- font-size: 0.7rem;
- color: var(--text-muted);
- margin-left: 8px;
-}
-
-/* --- Editor Modal --- */
-.editor-modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 1000;
- background: var(--overlay-bg);
- align-items: center;
- justify-content: center;
- padding: 20px;
-}
-.editor-modal.active {
- display: flex;
-}
-.help-container {
- max-width: 1320px;
-}
-.editor-container {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- width: 100%;
- max-width: 1200px;
- max-height: 90vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
-}
-.editor-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-primary);
- flex-shrink: 0;
- position: sticky;
- top: 0;
- z-index: 10;
-}
-
-/* AI processing indicator — shown inside editor header during AI calls */
-.ai-processing-indicator {
- display: none;
- align-items: center;
- gap: 6px;
- padding: 3px 10px;
- border-radius: 12px;
- font-size: 0.72rem;
- font-weight: 500;
- color: #ffffff;
- background: rgba(59, 130, 246, 0.3);
- border: 1px solid rgba(96, 165, 250, 0.4);
- animation: ai-indicator-pulse 1s ease-in-out infinite;
- margin-left: 12px;
-}
-
-.ai-spinner {
- width: 10px;
- height: 10px;
- border: 2px solid rgba(255,255,255,0.3);
- border-top-color: #ffffff;
- border-radius: 50%;
- animation: ai-spinner-spin 0.6s linear infinite;
-}
-
-@keyframes ai-spinner-spin {
- to { transform: rotate(360deg); }
-}
-
-@keyframes ai-indicator-pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.6; }
-}
-
-.editor-title {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.9rem;
- color: var(--text-primary);
- font-weight: 600;
-}
-.editor-brand { font-weight: 700; font-size: 12px; color: var(--accent); letter-spacing: 0.5px; white-space: nowrap; margin-right: 8px; }
-.editor-title-input { flex: 1; min-width: 80px; background: transparent; border: 1px solid transparent; color: var(--text-primary); font-size: 13px; padding: 3px 6px; border-radius: 6px; outline: none; font-family: var(--font-sans); }
-.editor-title-input:hover { border-color: var(--border); }
-.editor-title-input:focus { border-color: var(--accent); background: var(--bg-input); }
-.editor-sep { color: var(--text-muted); font-size: 13px; padding: 0 2px; user-select: none; }
-.editor-filename-input { width: 140px; background: transparent; border: 1px solid transparent; color: var(--text-primary); font-size: 12px; padding: 3px 6px; border-radius: 6px; outline: none; font-family: var(--font-mono); }
-.editor-filename-input:hover { border-color: var(--border); }
-.editor-filename-input:focus { border-color: var(--ok); background: var(--bg-input); }
-.editor-spacer { flex: 1; }
-.editor-save-dot { display: flex; align-items: center; gap: 4px; font-size: 11px; padding: 0 4px; margin-right: 8px; }
-.editor-btn + .editor-btn { margin-left: 6px; }
-.editor-save-dot .dot { width: 6px; height: 6px; border-radius: 50%; }
-.editor-save-dot.ok { color: var(--ok); } .editor-save-dot.ok .dot { background: var(--ok); }
-.editor-save-dot.dirty { color: var(--warn); } .editor-save-dot.dirty .dot { background: var(--warn); }
-.editor-save-dot.saving { color: var(--ai); } .editor-save-dot.saving .dot { background: var(--ai); }
-@keyframes editor-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
-.editor-save-dot.saving { animation: editor-pulse 0.8s infinite; }
-.editor-save-dot.err { color: var(--err); } .editor-save-dot.err .dot { background: var(--err); }
-.editor-body-cm { flex: 1; overflow: auto; }
-.editor-body-cm .cm-editor { height: 100%; }
-.editor-body-cm .cm-editor .cm-scroller { font-family: var(--font-mono); font-size: 13.5px; line-height: 1.75; }
-.editor-footer { height: 26px; min-height: 26px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; background: var(--bg-secondary); border-top: 1px solid var(--border); font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
-.editor-stat-file { color: var(--text-secondary); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.editor-actions {
- display: flex;
- gap: 6px;
-}
-.editor-btn {
- width: 36px;
- height: 36px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 150ms ease;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 auto;
-}
-.editor-btn:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-.editor-btn.primary {
- background: var(--accent);
- color: #ffffff;
- border-color: var(--accent);
-}
-.editor-btn.primary:hover {
- opacity: 0.9;
-}
-.editor-btn.danger:hover {
- color: var(--danger);
- border-color: var(--danger);
-}
-.editor-body {
- flex: 1;
- min-height: 0;
- position: relative;
- overflow-y: auto;
- overflow-x: hidden;
-}
-.help-body {
- padding: 0;
- background: radial-gradient(circle at top right, color-mix(in srgb, var(--accent) 10%, transparent), transparent 30%), var(--bg-primary);
- display: flex;
- gap: 0;
- overflow-y: auto;
- overflow-x: hidden;
- scroll-behavior: smooth;
-}
-
-/* Help Navigation Menu */
-.help-hamburger {
- display: none;
- width: 36px; height: 36px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-right: 8px;
-}
-.help-hamburger:hover { color: var(--accent); border-color: var(--accent); }
-.help-nav {
- width: 260px;
- min-width: 260px;
- background: color-mix(in srgb, var(--bg-secondary) 94%, var(--bg-primary));
- border-right: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
- display: flex;
- flex-direction: column;
- position: sticky;
- top: 0;
- align-self: flex-start;
- height: 100%;
- max-height: calc(90vh - 57px);
- flex-shrink: 0;
- backdrop-filter: blur(12px);
- overflow: hidden;
-}
-.help-nav-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20px 16px 10px;
- flex-shrink: 0;
-}
-.help-nav-title {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.75rem;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
-}
-.help-nav-search-wrap {
- padding: 0 12px 8px;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- gap: 0;
-}
-.help-nav-search {
- flex: 1;
- padding: 7px 10px;
- border: 1px solid var(--border);
- border-right: none;
- border-radius: 8px 0 0 8px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 0.78rem;
- outline: none;
- transition: border-color 0.2s;
- min-width: 0;
-}
-.help-nav-search:focus {
- border-color: var(--accent);
-}
-.help-nav-search:focus + .help-search-clear {
- border-color: var(--accent);
- border-left-color: var(--accent);
-}
-.help-nav-search::placeholder { color: var(--text-muted); }
-.help-search-clear {
- padding: 7px 10px;
- border: 1px solid var(--border);
- border-left: none;
- border-radius: 0 8px 8px 0;
- background: var(--bg-primary);
- color: var(--text-muted);
- cursor: pointer;
- font-size: 14px;
- line-height: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: color 0.15s, background 0.15s;
-}
-.help-search-clear:hover {
- color: var(--text-primary);
- background: rgba(255,255,255,0.06);
-}
-.help-nav-list {
- list-style: none;
- margin: 0;
- padding: 0 12px 20px;
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- scrollbar-width: thin;
- scrollbar-color: var(--border) transparent;
-}
-.help-nav-list li {
- margin: 0 0 3px;
-}
-.help-nav-link {
- display: block;
- padding: 8px 12px;
- font-size: 0.85rem;
- color: var(--text-secondary);
- text-decoration: none;
- transition: all 150ms ease;
- border: 1px solid transparent;
- border-radius: 8px;
- outline: none;
-}
-.help-nav-link:hover {
- color: var(--accent);
- background: color-mix(in srgb, var(--bg-hover) 80%, transparent);
- border-color: color-mix(in srgb, var(--accent) 16%, transparent);
- text-decoration: none;
- transform: translateX(2px);
-}
-.help-nav-link:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: -2px;
- border-radius: 8px;
-}
-.help-nav-link.active {
- color: var(--accent);
- background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, transparent), color-mix(in srgb, var(--accent) 7%, transparent));
- border-color: color-mix(in srgb, var(--accent) 22%, transparent);
- font-weight: 600;
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
-}
-.help-nav-link.hidden {
- display: none;
-}
-.help-section.help-hidden {
- display: none;
-}
-.help-search-highlight {
- background: rgba(250, 204, 21, 0.35);
- color: inherit;
- border-radius: 2px;
- padding: 0 1px;
-}
-
-.help-content {
- flex: 1;
- max-width: 980px;
- margin: 0 auto;
- padding: 32px 40px 56px;
- overflow: visible;
- min-width: 0;
-}
-.help-lead {
- font-size: 1.08rem;
- line-height: 1.75;
- color: var(--text-primary);
- margin-bottom: 18px;
-}
-.help-footer {
- margin-top: 48px;
- padding-top: 24px;
- border-top: 1px solid var(--border);
- text-align: center;
- color: var(--text-muted);
- font-size: 0.85rem;
-}
-.help-footer a {
- color: var(--accent);
- text-decoration: none;
-}
-.help-footer a:hover {
- text-decoration: underline;
-}
-.help-content h1,
-.help-content h2,
-.help-content h3 {
- font-family: "JetBrains Mono", monospace;
- color: var(--text-primary);
-}
-.help-content h1 {
- font-size: 2rem;
- margin-bottom: 16px;
- letter-spacing: -0.02em;
-}
-.help-content h2 {
- font-size: 1.18rem;
- margin: 0 0 14px;
-}
-.help-content h3 {
- font-size: 0.95rem;
- margin: 20px 0 8px;
- color: var(--text-secondary);
-}
-.help-content p {
- margin: 0 0 14px;
- color: var(--text-secondary);
- line-height: 1.7;
-}
-.help-content pre,
-.help-content table {
- max-width: 100%;
- overflow-x: auto;
-}
-.help-content pre code {
- white-space: pre;
-}
-.help-content ul,
-.help-content ol {
- margin: 10px 0 18px;
- padding-left: 24px;
- color: var(--text-secondary);
-}
-.help-content ul li,
-.help-content ol li {
- margin: 6px 0;
- line-height: 1.6;
-}
-.help-content code {
- font-family: "JetBrains Mono", monospace;
- background: var(--code-bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- padding: 1px 5px;
- color: var(--text-primary);
- font-size: 0.88em;
-}
-.help-content strong {
- color: var(--text-primary);
- font-weight: 600;
-}
-
-.help-section {
- position: relative;
- margin-bottom: 22px;
- padding: 24px 26px;
- border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
- border-radius: 18px;
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, var(--bg-primary)), color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary)));
- box-shadow: 0 14px 34px rgba(0, 0, 0, 0.18);
-}
-
-.help-section::before {
- content: "";
- position: absolute;
- inset: 0;
- border-radius: inherit;
- pointer-events: none;
- background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent 35%);
-}
-
-.help-hero {
- display: grid;
- grid-template-columns: minmax(0, 1.5fr) minmax(260px, 1fr);
- gap: 22px;
- align-items: stretch;
- padding: 22px;
- border: 1px solid color-mix(in srgb, var(--accent) 16%, var(--border));
- border-radius: 18px;
- background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 12%, transparent), transparent 45%), color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary));
- margin-bottom: 26px;
-}
-
-.help-kicker {
- display: inline-flex;
- align-items: center;
- padding: 5px 10px;
- border-radius: 999px;
- background: color-mix(in srgb, var(--accent) 16%, transparent);
- color: var(--accent);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.72rem;
- margin-bottom: 12px;
-}
-
-.help-badges {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 16px;
-}
-
-.help-badge {
- display: inline-flex;
- align-items: center;
- padding: 6px 10px;
- border-radius: 999px;
- background: color-mix(in srgb, var(--bg-primary) 72%, transparent);
- border: 1px solid var(--border);
- color: var(--text-primary);
- font-size: 0.8rem;
-}
-
-.help-hero-visual {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.help-mini-window {
- width: 100%;
- max-width: 320px;
- border-radius: 18px;
- overflow: hidden;
- border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border));
- background: color-mix(in srgb, var(--bg-primary) 88%, black 12%);
- box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
-}
-
-.help-mini-window-bar {
- display: flex;
- gap: 7px;
- padding: 12px 14px;
- border-bottom: 1px solid var(--border);
-}
-
-.help-mini-window-bar span {
- width: 9px;
- height: 9px;
- border-radius: 50%;
- background: color-mix(in srgb, var(--accent) 45%, var(--text-muted));
-}
-
-.help-mini-window-body {
- display: flex;
- min-height: 190px;
-}
-
-.help-mini-sidebar {
- width: 72px;
- background: color-mix(in srgb, var(--bg-secondary) 92%, black 8%);
- border-right: 1px solid var(--border);
-}
-
-.help-mini-content {
- flex: 1;
- padding: 16px;
-}
-
-.help-mini-line {
- height: 10px;
- border-radius: 999px;
- background: color-mix(in srgb, var(--text-muted) 28%, transparent);
- margin-bottom: 10px;
-}
-
-.help-mini-line.short {
- width: 62%;
-}
-
-.help-mini-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin: 14px 0;
-}
-
-.help-mini-tags span {
- padding: 3px 8px;
- border-radius: 999px;
- background: color-mix(in srgb, var(--accent) 16%, transparent);
- color: var(--text-primary);
- font-size: 0.72rem;
-}
-
-.help-mini-card {
- height: 64px;
- border-radius: 12px;
- border: 1px solid var(--border);
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, white 6%), var(--bg-primary));
-}
-
-.help-grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 16px;
- margin-bottom: 26px;
-}
-
-.help-card {
- padding: 18px;
- border-radius: 16px;
- border: 1px solid var(--border);
- background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary));
-}
-
-.help-card-icon {
- width: 42px;
- height: 42px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border-radius: 12px;
- background: color-mix(in srgb, var(--accent) 14%, transparent);
- font-size: 1.2rem;
- margin-bottom: 10px;
-}
-
-.help-section {
- margin-top: 26px;
-}
-
-.help-feature-list,
-.help-steps,
-.help-checklist,
-.help-tip-grid {
- display: grid;
- gap: 14px;
-}
-
-.help-feature-item,
-.help-step,
-.help-check-item,
-.help-tip-card {
- border: 1px solid var(--border);
- border-radius: 16px;
- background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary));
-}
-
-.help-feature-item {
- display: grid;
- grid-template-columns: 52px minmax(0, 1fr);
- gap: 14px;
- padding: 16px;
-}
-
-.help-feature-emoji {
- width: 52px;
- height: 52px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 14px;
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- font-size: 1.4rem;
-}
-
-.help-steps {
- counter-reset: step;
-}
-
-.help-step {
- display: grid;
- grid-template-columns: 44px minmax(0, 1fr);
- gap: 14px;
- padding: 16px;
-}
-
-.help-step-number {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: color-mix(in srgb, var(--accent) 18%, transparent);
- color: var(--accent);
- font-family: "JetBrains Mono", monospace;
- font-weight: 700;
-}
-
-.help-check-item {
- display: grid;
- grid-template-columns: 28px minmax(0, 1fr);
- gap: 12px;
- padding: 14px 16px;
- align-items: start;
-}
-
-.help-check-item span {
- font-size: 1rem;
- line-height: 1.4;
-}
-
-.help-check-item p {
- margin-bottom: 0;
-}
-
-.help-tip-panel {
- margin-top: 28px;
-}
-
-.help-tip-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
-}
-
-.help-tip-card {
- padding: 16px;
-}
-.cm-editor {
- height: auto;
- min-height: 100%;
- font-size: 0.9rem;
-}
-.cm-scroller {
- font-family: "JetBrains Mono", monospace;
- overflow-y: auto !important;
- overflow-x: auto !important;
- height: auto;
- min-height: 100%;
- max-width: 100%;
-}
-.fallback-editor {
- width: 100%;
- min-height: 100%;
- height: auto;
- border: none;
- outline: none;
- resize: none;
- padding: 16px;
- background: var(--code-bg);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.9rem;
- line-height: 1.55;
- box-sizing: border-box;
- overflow-y: auto;
- overflow-x: auto;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-/* Mobile right sidebar - hide by default */
-@media (max-width: 768px) {
- .right-sidebar {
- display: none;
- }
- .right-sidebar-resize-handle {
- display: none;
- }
-}
-
-/* Mobile help navigation */
-@media (max-width: 768px) {
- .help-nav {
- display: none;
- }
- .help-content {
- padding: 20px 16px 32px;
- }
- .vault-context-indicator {
- /* Keep visible on mobile — same look as desktop */
- padding: 4px 8px;
- font-size: 0.7rem;
- }
-}
-
-/* Mobile editor */
-@media (max-width: 768px) {
- .editor-modal {
- padding: 0;
- align-items: stretch;
- }
- .editor-container {
- max-width: 100%;
- max-height: 100vh;
- border-radius: 0;
- height: 100vh;
- overflow: hidden;
- }
- .editor-header {
- padding: 10px 12px;
- position: sticky;
- top: 0;
- z-index: 20;
- background: var(--bg-primary);
- }
- .editor-title {
- font-size: 0.85rem;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .editor-actions {
- gap: 4px;
- }
- .editor-btn {
- width: 36px;
- height: 36px;
- min-width: 36px;
- }
- .editor-body {
- flex: 1;
- min-height: 0;
- overflow: auto;
- }
- .cm-editor {
- height: auto;
- min-height: 100%;
- }
- .cm-scroller {
- overflow: auto !important;
- height: auto;
- min-height: 100%;
- }
- .fallback-editor {
- min-height: 100%;
- height: auto;
- }
- .search-results-header {
- align-items: flex-start;
- }
-}
-
-/* --- No-select during resize --- */
-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 {
- width: 240px;
- min-width: 200px;
- }
-}
-
-/* --- Responsive — Mobile --- */
-@media (max-width: 768px) {
- .hamburger-btn {
- display: flex;
- }
-
- .sidebar-toggle-btn {
- display: none;
- }
-
- .header {
- flex-wrap: wrap;
- gap: 8px 10px;
- padding: 10px 12px;
- }
-
- .header-left {
- flex: 1;
- gap: 12px;
- }
-
- .header-center {
- flex: 0 0 100%;
- order: 3;
- max-width: none;
- padding: 4px 0;
- }
-
- .header-right {
- flex: 0 0 auto;
- gap: 8px;
- }
-
- .header-logo {
- font-size: 1.05rem;
- gap: 8px;
- }
-
- .user-display-name {
- display: none; /* Masquer le nom d'utilisateur sur mobile pour gagner de la place */
- }
-
- .search-wrapper {
- max-width: none;
- width: 100%;
- }
-
- .search-input-wrapper input {
- padding: 10px 40px 10px 40px;
- font-size: 1rem;
- border-radius: 12px;
- }
-
- .search-wrapper .search-icon {
- left: 14px;
- top: 14px;
- width: 18px !important;
- height: 18px !important;
- }
-
- /* Mobile: keep only filter + clear visible in header search */
- .search-actions .tog,
- .search-actions .search-sep,
- .search-actions .search-count { display: none; }
- .search-actions { gap: 4px; }
- .search-actions .search-btn#search-clear-btn { display: flex; }
-
- .search-dropdown {
- position: fixed;
- top: 100px;
- left: 8px;
- right: 8px;
- width: calc(100vw - 16px);
- max-height: calc(100vh - 120px);
- border-radius: 12px;
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
- }
-
- .search-dropdown__item {
- padding: 14px 16px;
- font-size: 0.95rem;
- }
-
- .search-dropdown__icon {
- width: 18px;
- height: 18px;
- margin-right: 8px;
- }
-
- .header-menu-btn {
- padding: 10px;
- border-radius: 10px;
- }
-
- .header-menu-dropdown {
- position: fixed;
- top: 60px;
- right: 10px;
- left: 10px;
- width: auto;
- min-width: 0;
- border-radius: 16px;
- }
-
- .sidebar-quick-select {
- padding: 0 16px 12px;
- }
-
- .sidebar-quick-select select {
- color-scheme: dark;
- background-color: var(--bg-secondary);
- color: var(--text-primary);
- }
-
- [data-theme="dark"] .sidebar-quick-select select {
- background-color: #1e1e1e;
- color: #e0e0e0;
- }
-
- .sidebar-panel-toggle {
- padding: 8px 12px 10px;
- }
-
- .tag-cloud {
- padding: 0 12px 12px;
- }
-
- .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);
- }
-
- /* Mobile sidebar overlay */
- .sidebar-overlay {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.4);
- z-index: 199;
- }
- .sidebar-overlay.active { display: block; }
-
- /* Right Sidebar handling on mobile */
- .right-sidebar {
- position: fixed;
- top: 0;
- right: 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;
- border-left: 1px solid var(--border);
- }
- .right-sidebar.hidden {
- transform: translateX(100%) !important;
- width: 280px !important;
- min-width: 280px !important;
- display: flex;
- }
- .right-sidebar:not(.hidden) {
- transform: translateX(0) !important;
- box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3) !important;
- display: flex !important;
- visibility: visible !important;
- }
-
- .right-sidebar-resize-handle {
- display: none;
- }
-
- .sidebar-resize-handle {
- display: none;
- }
-
- .tag-resize-handle {
- display: none;
- }
-
- .tag-cloud-section {
- height: auto;
- max-height: 200px;
- min-height: 80px;
- }
-
- #vault-panel-content {
- max-height: none;
- }
- #tag-panel-content {
- max-height: none;
- }
-
- .help-content {
- padding: 20px 16px 28px;
- }
-
- .help-hero {
- grid-template-columns: 1fr;
- padding: 18px;
- }
-
- .help-grid,
- .help-tip-grid {
- grid-template-columns: 1fr;
- }
-
- .content-area {
- padding: 12px 8px 24px;
- width: 100%;
- }
-
- .file-title {
- font-size: 1.15rem;
- }
-
- .file-actions {
- flex-wrap: wrap;
- gap: 4px;
- }
-
- .file-actions .btn-action {
- padding: 6px 8px;
- font-size: 0.75rem;
- }
-
- /* Full-width search results */
- .search-result-item {
- padding: 12px 14px;
- border-radius: 8px;
- margin-bottom: 8px;
- }
-
- .search-result-title {
- font-size: 0.9rem;
- padding-right: 40px;
- }
-
- .search-result-ext {
- top: 6px;
- right: 8px;
- font-size: 0.6rem;
- }
-
- .search-results-header {
- flex-wrap: wrap;
- gap: 6px;
- }
-
- /* Full-width dashboard */
- .dashboard-stats-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 8px;
- }
-
- .stat-card {
- padding: 12px 8px;
- }
-
- .dashboard-tab {
- padding: 6px 5px !important;
- font-size: 0 !important;
- gap: 0 !important;
- overflow: hidden;
- min-width: 32px;
- }
- .dashboard-tab > *:not(i):not(svg) { display: none; }
- .dashboard-tab i, .dashboard-tab svg {
- width: 18px !important;
- height: 18px !important;
- flex-shrink: 0;
- }
-
- .breadcrumb {
- font-size: 0.75rem;
- flex-wrap: wrap;
- }
-
- /* Mobile tab bar */
- .tab-bar {
- padding: 0 4px;
- }
- .tab-item {
- padding: 6px 10px;
- font-size: 0.7rem;
- }
- .tab-item .tab-name { max-width: 80px; }
- .tab-item .tab-close { opacity: 0.6; width: 14px; height: 14px; }
-
- .md-content {
- padding: 0 4px;
- }
-
- .md-content h1 { font-size: 1.4rem; }
- .md-content h2 { font-size: 1.15rem; }
- .md-content h3 { font-size: 1rem; }
-
- .md-content pre {
- margin: 8px -4px;
- border-radius: 6px;
- padding: 10px 12px;
- font-size: 0.8rem;
- }
-
- /* Full-width content wrapper */
- .content-wrapper {
- margin-left: 0 !important;
- }
-}
-
-/* --- Configuration Modal --- */
-.config-content {
- max-width: 720px;
- margin: 0 auto;
- padding: 28px 24px 40px;
-}
-
-.config-section {
- margin-bottom: 28px;
-}
-
-.config-section h2 {
- font-family: "JetBrains Mono", monospace;
- font-size: 1.05rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
-}
-
-.config-description {
- font-size: 0.85rem;
- color: var(--text-secondary);
- margin-bottom: 16px;
- line-height: 1.5;
-}
-
-.config-filters-list {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 16px;
- min-height: 32px;
-}
-
-.config-filter-badge {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- background: var(--tag-bg);
- color: var(--tag-text);
- border-radius: 6px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- border: 1px solid color-mix(in srgb, var(--tag-text) 30%, transparent);
-}
-
-.config-filter-badge.disabled {
- opacity: 0.5;
-}
-
-.config-filter-remove {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border: none;
- background: transparent;
- color: inherit;
- cursor: pointer;
- padding: 0;
- font-size: 1.1rem;
- line-height: 1;
-}
-
-.config-filter-remove:hover {
- opacity: 0.7;
-}
-
-.config-filter-toggle {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border: none;
- background: transparent;
- color: inherit;
- cursor: pointer;
- padding: 0;
- font-size: 0.9rem;
-}
-
-.config-add-pattern {
- display: flex;
- gap: 8px;
- margin-bottom: 12px;
-}
-
-.config-input {
- flex: 1;
- padding: 8px 12px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- outline: none;
- transition: border-color 200ms ease;
-}
-
-.config-input:focus {
- border-color: var(--accent);
-}
-
-.config-select {
- padding: 8px 12px;
- border: 1px solid var(--border);
- border-radius: var(--r);
- background: var(--bg-input);
- color: var(--text-primary);
- font-size: 0.86rem;
- font-family: var(--mono);
- cursor: pointer;
-}
-
-.config-btn-add {
- padding: 8px 16px;
- border: 1px solid var(--accent);
- border-radius: 6px;
- background: var(--accent);
- color: #ffffff;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- font-weight: 600;
- cursor: pointer;
- transition: opacity 150ms ease;
-}
-
-.config-btn-add:hover {
- opacity: 0.9;
-}
-
-.config-regex-preview {
- padding: 8px 12px;
- background: var(--code-bg);
- border: 1px solid var(--border);
- border-radius: 6px;
- font-size: 0.75rem;
- color: var(--text-secondary);
-}
-
-.config-regex-preview code {
- font-family: "JetBrains Mono", monospace;
- color: var(--accent);
- background: transparent;
- border: none;
- padding: 0;
-}
-
-/* --- Config rows & controls --- */
-.config-row {
- display: grid;
- grid-template-columns: 200px 120px 1fr;
- align-items: center;
- gap: 12px;
- margin-bottom: 10px;
-}
-.config-label {
- font-size: 0.82rem;
- color: var(--text-primary);
- font-weight: 500;
-}
-.config-input--num {
- flex: none;
- width: 120px;
- text-align: right;
-}
-.config-hint {
- font-size: 0.72rem;
- color: var(--text-muted);
-}
-.config-badge-restart {
- display: inline-block;
- font-size: 0.65rem;
- font-weight: 500;
- padding: 2px 8px;
- border-radius: 4px;
- background: var(--danger-bg, #3d1a18);
- color: var(--danger, #ff7b72);
- vertical-align: middle;
- margin-left: 6px;
-}
-.config-actions-row {
- display: flex;
- gap: 8px;
- margin-top: 16px;
- flex-wrap: wrap;
-}
-.config-btn-save {
- padding: 8px 20px;
- border: 1px solid var(--accent);
- border-radius: 6px;
- background: var(--accent);
- color: #fff;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- font-weight: 600;
- cursor: pointer;
- transition: opacity 150ms;
-}
-.config-btn-save:hover {
- opacity: 0.9;
-}
-.config-btn-save.config-btn-highlight {
- background: var(--accent-green);
- border-color: var(--accent-green);
- animation: pulse 2s ease-in-out infinite;
-}
-@keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.8;
- }
-}
-.config-btn-secondary {
- padding: 8px 16px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- cursor: pointer;
- transition: background 150ms;
-}
-.config-btn-secondary:hover {
- background: var(--bg-hover);
-}
-
-/* --- Config diagnostics panel --- */
-.config-diagnostics {
- background: var(--code-bg);
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 14px 16px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.78rem;
- line-height: 1.7;
- color: var(--text-secondary);
-}
-.config-diag-loading {
- color: var(--text-muted);
-}
-.config-diag-row {
- display: flex;
- justify-content: space-between;
-}
-.config-diag-row .diag-label {
- color: var(--text-secondary);
-}
-.config-diag-row .diag-value {
- color: var(--text-primary);
- font-weight: 500;
-}
-.config-diag-section {
- margin-bottom: 8px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border);
-}
-.config-diag-section:last-child {
- margin-bottom: 0;
- padding-bottom: 0;
- border-bottom: none;
-}
-.config-diag-section-title {
- font-weight: 600;
- color: var(--accent);
- margin-bottom: 4px;
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-@media (max-width: 768px) {
- .config-row {
- grid-template-columns: 1fr;
- gap: 4px;
- }
- .config-input--num {
- width: 100%;
- }
-}
-
-/* --- Toast notifications --- */
-.toast-container {
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 10000;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- pointer-events: none;
-}
-.toast {
- padding: 10px 20px;
- border-radius: 8px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.82rem;
- color: var(--text-primary);
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
- opacity: 0;
- transform: translateY(12px);
- transition:
- opacity 250ms ease,
- transform 250ms ease;
- pointer-events: auto;
- max-width: 420px;
- text-align: center;
-}
-.toast.show {
- opacity: 1;
- transform: translateY(0);
-}
-.toast-error {
- border-color: var(--danger);
- background: var(--danger-bg);
- color: var(--danger);
-}
-.toast-success {
- border-color: var(--success);
- background: var(--success-bg);
- color: var(--success);
-}
-
-/* --- Tree loading indicator --- */
-.tree-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 8px 16px;
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Autocomplete Dropdown
- --------------------------------------------------------------------------- */
-.search-dropdown {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- margin-top: 4px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
- z-index: 200;
-}
-.search-dropdown[hidden] {
- display: none;
-}
-.search-dropdown__section {
- border-bottom: 1px solid var(--border);
-}
-.search-dropdown__section:last-child {
- border-bottom: none;
-}
-.search-dropdown__section[hidden] {
- display: none;
-}
-.search-dropdown__section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 12px;
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- background: var(--bg-primary);
- position: sticky;
- top: 0;
- z-index: 1;
-}
-.search-dropdown__clear-btn {
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- padding: 2px 4px;
- border-radius: 4px;
- display: flex;
- align-items: center;
-}
-.search-dropdown__clear-btn:hover {
- color: var(--danger);
- background: var(--danger-bg, rgba(255, 0, 0, 0.08));
-}
-.search-dropdown__list {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-.search-dropdown__item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 7px 12px;
- cursor: pointer;
- font-size: 0.82rem;
- color: var(--text-primary);
- transition: background 120ms ease;
-}
-.search-dropdown__item:hover,
-.search-dropdown__item.active {
- background: var(--bg-hover);
-}
-.search-dropdown__item--history {
- color: var(--text-secondary);
-}
-.search-dropdown__item--history .search-dropdown__icon {
- color: var(--text-muted);
-}
-.search-dropdown__item--title .search-dropdown__meta {
- font-size: 0.72rem;
- color: var(--text-muted);
- margin-left: auto;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 180px;
-}
-.search-dropdown__item--tag .search-dropdown__badge {
- margin-left: auto;
- font-size: 0.7rem;
- color: var(--text-muted);
- background: var(--bg-primary);
- padding: 1px 6px;
- border-radius: 10px;
-}
-.search-dropdown__icon {
- flex-shrink: 0;
- width: 14px;
- height: 14px;
- color: var(--text-muted);
-}
-.search-dropdown__text {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.search-dropdown__text mark {
- background: var(--accent);
- color: var(--bg-primary);
- border-radius: 2px;
- padding: 0 1px;
-}
-.search-dropdown__empty {
- padding: 16px 12px;
- text-align: center;
- font-size: 0.82rem;
- color: var(--text-muted);
-}
-.search-dropdown__empty[hidden] {
- display: none;
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Filter Chips
- --------------------------------------------------------------------------- */
-.search-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- padding: 6px 0 0;
-}
-.search-chips[hidden] {
- display: none;
-}
-.search-chip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 8px;
- font-size: 0.75rem;
- font-family: "JetBrains Mono", monospace;
- border-radius: 12px;
- background: var(--accent);
- color: var(--bg-primary);
- cursor: default;
- max-width: 200px;
- white-space: nowrap;
-}
-.search-chip__label {
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.search-chip__remove {
- display: flex;
- align-items: center;
- justify-content: center;
- background: none;
- border: none;
- color: inherit;
- cursor: pointer;
- padding: 0;
- opacity: 0.7;
- transition: opacity 120ms;
-}
-.search-chip__remove:hover {
- opacity: 1;
-}
-.search-chip--tag {
- background: var(--accent);
- color: var(--bg-primary);
-}
-.search-chip--vault {
- background: var(--success, #22c55e);
- color: #fff;
-}
-.search-chip--title {
- background: var(--warning, #f59e0b);
- color: #1a1a1a;
-}
-.search-chip--path {
- background: var(--text-muted);
- color: var(--bg-primary);
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Snippet Highlights
- --------------------------------------------------------------------------- */
-.search-result__snippet mark {
- background: var(--accent);
- color: var(--bg-primary);
- border-radius: 2px;
- padding: 0 2px;
- font-weight: 600;
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Facets Panel
- --------------------------------------------------------------------------- */
-.search-facets {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- margin-bottom: 12px;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: 8px;
-}
-.search-facets[hidden] {
- display: none;
-}
-.search-facets__group {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
-}
-.search-facets__label {
- font-size: 0.72rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- margin-right: 4px;
-}
-.search-facets__item {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 8px;
- font-size: 0.75rem;
- border: 1px solid var(--border);
- border-radius: 12px;
- color: var(--text-secondary);
- background: var(--bg-secondary);
- cursor: pointer;
- transition:
- border-color 120ms,
- background 120ms;
-}
-.search-facets__item:hover {
- border-color: var(--accent);
- background: var(--bg-hover);
-}
-.search-facets__item .facet-count {
- font-size: 0.68rem;
- color: var(--text-muted);
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Pagination
- --------------------------------------------------------------------------- */
-.search-pagination {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px 0;
-}
-.search-pagination__btn {
- padding: 6px 14px;
- font-size: 0.82rem;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- cursor: pointer;
- transition:
- border-color 120ms,
- background 120ms;
-}
-.search-pagination__btn:hover:not(:disabled) {
- border-color: var(--accent);
- background: var(--bg-hover);
-}
-.search-pagination__btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-.search-pagination__info {
- font-size: 0.78rem;
- color: var(--text-muted);
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Sort Controls
- --------------------------------------------------------------------------- */
-.search-sort {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-left: auto;
-}
-.search-sort__btn {
- padding: 3px 10px;
- font-size: 0.75rem;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 120ms;
-}
-.search-sort__btn.active {
- border-color: var(--accent);
- color: var(--accent);
- background: var(--bg-hover);
-}
-
-/* ---------------------------------------------------------------------------
- Advanced Search — Responsive
- --------------------------------------------------------------------------- */
-@media (max-width: 768px) {
- .search-dropdown {
- max-height: 280px;
- }
- .search-facets {
- flex-direction: column;
- gap: 8px;
- }
- .search-chip {
- max-width: 150px;
- }
-}
-
-/* ---------------------------------------------------------------------------
- Sync Badge — real-time connection status indicator
- --------------------------------------------------------------------------- */
-.sync-badge {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- margin-left: 8px;
- cursor: pointer;
- transition:
- background 300ms,
- box-shadow 300ms;
- flex-shrink: 0;
- vertical-align: middle;
-}
-.sync-badge--disconnected {
- background: #ef4444;
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.25);
-}
-.sync-badge--connecting {
- background: #f59e0b;
- box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25);
- animation: sync-pulse 1.5s ease-in-out infinite;
-}
-.sync-badge--connected {
- background: #22c55e;
- box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
-}
-.sync-badge--syncing {
- background: #3b82f6;
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
- animation: sync-pulse 0.8s ease-in-out infinite;
-}
-@keyframes sync-pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.4;
- }
-}
-
-/* AI processing — white flash on the sync badge */
-.sync-badge--ai-processing {
- background: #ffffff !important;
- box-shadow: 0 0 6px 3px rgba(255, 255, 255, 0.6) !important;
- animation: ai-flash 0.6s ease-in-out infinite;
-}
-
-@keyframes ai-flash {
- 0%, 100% { opacity: 1; box-shadow: 0 0 6px 3px rgba(255,255,255,0.6); }
- 50% { opacity: 0.3; box-shadow: 0 0 12px 6px rgba(255,255,255,0.9); }
-}
-
-/* ── AI Status section inside sync panel ── */
-
-.sync-panel__ai-section {
- border-top: 1px solid var(--border);
- padding: 10px 14px;
-}
-
-.sync-panel__ai-title {
- font-size: 0.78rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
-}
-
-.sync-panel__ai-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 5px;
-}
-
-.sync-panel__ai-label {
- font-size: 0.72rem;
- color: var(--text-muted);
-}
-
-.sync-panel__ai-state {
- font-size: 0.75rem;
- display: flex;
- align-items: center;
- gap: 5px;
- padding: 2px 8px;
- border-radius: 4px;
-}
-
-.sync-panel__ai-state i {
- flex-shrink: 0;
-}
-
-.sync-panel__ai--active {
- color: #22c55e;
- background: rgba(34, 197, 94, 0.1);
-}
-
-.sync-panel__ai--inactive {
- color: #f59e0b;
- background: rgba(245, 158, 11, 0.1);
-}
-
-.sync-panel__ai--processing {
- color: #ffffff;
- background: rgba(59, 130, 246, 0.2);
- animation: ai-status-pulse 1s ease-in-out infinite;
-}
-
-.sync-panel__ai--unknown {
- color: var(--text-muted);
- background: var(--bg-tertiary);
-}
-
-@keyframes ai-status-pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.6; }
-}
-
-/* ---------------------------------------------------------------------------
- Vault Home Page
- --------------------------------------------------------------------------- */
-
-.vault-home {
- padding: 16px 24px;
- max-width: 900px;
- margin: 0 auto;
-}
-
-.vault-home-breadcrumb {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 2px;
- font-size: 0.85rem;
- margin-bottom: 16px;
- padding: 8px 12px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 8px;
-}
-
-.vault-home-breadcrumb-item {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 6px;
- border-radius: 4px;
- color: var(--text-secondary);
- transition: background 0.15s, color 0.15s;
- user-select: none;
-}
-
-.vault-home-breadcrumb-item:hover {
- background: color-mix(in srgb, var(--accent) 10%, transparent);
- color: var(--text-primary);
-}
-
-.vault-home-breadcrumb-root {
- font-weight: 600;
- color: var(--accent);
-}
-
-.vault-home-breadcrumb-sep {
- color: var(--text-muted);
- user-select: none;
- font-size: 0.8rem;
-}
-
-.vault-home-header {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border);
-}
-
-.vault-home-count {
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-primary);
- background: var(--bg-secondary);
- padding: 3px 10px;
- border-radius: 4px;
-}
-
-.vault-home-recursive-badge {
- font-size: 0.7rem;
- font-weight: 600;
- color: var(--accent);
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- padding: 2px 8px;
- border-radius: 4px;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.vault-home-dir-info {
- font-size: 0.75rem;
- color: var(--text-muted);
- font-family: var(--font-mono);
-}
-
-.vault-home-results {
- min-height: 200px;
-}
-
-.vault-home-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 3rem;
- color: var(--text-muted);
- gap: 8px;
-}
-
-.vault-home-empty p {
- font-size: 0.85rem;
- margin: 0;
-}
-
-.vault-home-error {
- padding: 2rem;
- text-align: center;
- color: var(--danger, #ef4444);
- font-size: 0.85rem;
-}
-
-/* ── Recent files section on vault home ── */
-.vault-home-recent {
- margin-bottom: 20px;
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 8px;
-}
-
-.vault-home-recent-header {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--text-muted);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-bottom: 8px;
- padding-bottom: 6px;
- border-bottom: 1px solid var(--border);
-}
-
-.vault-home-recent-list {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.vault-home-recent-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 5px 6px;
- border-radius: 4px;
- cursor: pointer;
- transition: background 0.12s;
- font-size: 0.82rem;
-}
-
-.vault-home-recent-item:hover {
- background: color-mix(in srgb, var(--accent) 8%, transparent);
-}
-
-.vault-home-recent-title {
- color: var(--text-primary);
- font-weight: 500;
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.vault-home-recent-dir {
- color: var(--text-muted);
- font-size: 0.72rem;
- font-family: var(--font-mono);
- max-width: 180px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.vault-home-recent-time {
- color: var(--text-muted);
- font-size: 0.7rem;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.vault-home-item {
- cursor: pointer;
-}
-
-.vault-home-item:hover {
- background: var(--bg-secondary);
-}
-
-/* ---------------------------------------------------------------------------
- Sync Panel — event log overlay
- --------------------------------------------------------------------------- */
-.sync-panel {
- position: fixed;
- top: 52px;
- left: 16px;
- width: 340px;
- max-height: 400px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 10px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- z-index: 9999;
- overflow: hidden;
- font-size: 0.82rem;
-}
-.sync-panel__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 14px;
- border-bottom: 1px solid var(--border);
-}
-.sync-panel__title {
- font-weight: 600;
- color: var(--text-primary);
-}
-.sync-panel__state {
- font-size: 0.72rem;
- padding: 2px 8px;
- border-radius: 8px;
- font-weight: 500;
-}
-.sync-panel__state--connected {
- background: rgba(34, 197, 94, 0.15);
- color: #22c55e;
-}
-.sync-panel__state--disconnected {
- background: rgba(239, 68, 68, 0.15);
- color: #ef4444;
-}
-.sync-panel__state--connecting {
- background: rgba(245, 158, 11, 0.15);
- color: #f59e0b;
-}
-.sync-panel__state--syncing {
- background: rgba(59, 130, 246, 0.15);
- color: #3b82f6;
-}
-.sync-panel__empty {
- padding: 20px 14px;
- text-align: center;
- color: var(--text-muted);
-}
-.sync-panel__events {
- max-height: 320px;
- overflow-y: auto;
- padding: 4px 0;
-}
-.sync-panel__event {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 7px 14px;
- border-bottom: 1px solid var(--border);
-}
-.sync-panel__event:last-child {
- border-bottom: none;
-}
-.sync-panel__event-type {
- font-weight: 500;
- color: var(--text-primary);
- white-space: nowrap;
-}
-.sync-panel__event-detail {
- color: var(--text-secondary);
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.sync-panel__event-time {
- font-size: 0.7rem;
- color: var(--text-muted);
- white-space: nowrap;
-}
-
-/* ---------------------------------------------------------------------------
- Toast — info variant (blue)
- --------------------------------------------------------------------------- */
-.toast.toast-info {
- background: #1e3a5f;
- border-left: 4px solid #3b82f6;
- color: #bfdbfe;
-}
-[data-theme="light"] .toast.toast-info {
- background: #eff6ff;
- border-left: 4px solid #3b82f6;
- color: #1e40af;
-}
-
-/* ---------------------------------------------------------------------------
- Config Toggle Switch
- --------------------------------------------------------------------------- */
-.config-toggle {
- position: relative;
- display: inline-block;
- width: 40px;
- height: 22px;
- flex-shrink: 0;
-}
-.config-toggle input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-.config-toggle-slider {
- position: absolute;
- cursor: pointer;
- inset: 0;
- background: var(--bg-hover);
- border: 1px solid var(--border);
- border-radius: 22px;
- transition:
- background 200ms,
- border-color 200ms;
-}
-.config-toggle-slider::before {
- content: "";
- position: absolute;
- height: 16px;
- width: 16px;
- left: 2px;
- bottom: 2px;
- background: var(--text-muted);
- border-radius: 50%;
- transition:
- transform 200ms,
- background 200ms;
-}
-.config-toggle input:checked + .config-toggle-slider {
- background: rgba(34, 197, 94, 0.2);
- border-color: #22c55e;
-}
-.config-toggle input:checked + .config-toggle-slider::before {
- transform: translateX(18px);
- background: #22c55e;
-}
-
-/* ---------------------------------------------------------------------------
- Hidden Files Configuration
- --------------------------------------------------------------------------- */
-.hidden-files-vault-card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 12px;
- margin-bottom: 12px;
- transition: border-color 200ms ease;
-}
-
-.hidden-files-vault-card:hover {
- border-color: var(--accent);
-}
-
-.hidden-files-vault-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border);
-}
-
-.hidden-files-vault-header h3 {
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.hidden-files-vault-type {
- font-size: 0.7rem;
- padding: 2px 6px;
- background: var(--tag-bg);
- color: var(--tag-text);
- border-radius: 3px;
- font-family: var(--mono);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.hidden-files-whitelist {
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px solid var(--border);
-}
-
-.hidden-files-whitelist-items {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 10px;
- min-height: 28px;
- padding: 4px;
- background: var(--bg-primary);
- border-radius: 4px;
- border: 1px dashed var(--border);
-}
-
-.hidden-files-whitelist-items:empty::before {
- content: "Aucun dossier dans la liste blanche";
- color: var(--text-muted);
- font-size: 0.85rem;
- font-style: italic;
-}
-
-.hidden-files-whitelist-item {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 6px 3px 8px;
- background: var(--tag-bg);
- color: var(--tag-text);
- border-radius: 4px;
- font-size: 0.85rem;
- font-family: var(--mono);
- border: 1px solid color-mix(in srgb, var(--tag-text) 30%, transparent);
- transition: all 150ms ease;
-}
-
-.hidden-files-whitelist-item:hover {
- background: color-mix(in srgb, var(--tag-bg) 80%, var(--accent) 20%);
- border-color: var(--accent);
-}
-
-.whitelist-item-text {
- user-select: none;
-}
-
-.whitelist-item-remove {
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- font-size: 1.1rem;
- line-height: 1;
- padding: 0 2px;
- transition: all 150ms ease;
- border-radius: 2px;
-}
-
-.whitelist-item-remove:hover {
- color: var(--danger);
- background: color-mix(in srgb, var(--danger) 15%, transparent);
-}
-
-.hidden-files-add-row {
- display: flex;
- gap: 8px;
- align-items: center;
- margin-top: 4px;
-}
-
-.hidden-files-add-row .config-input {
- flex: 1;
- max-width: 250px;
- font-family: var(--mono);
- font-size: 0.9rem;
-}
-
-.config-btn-add {
- padding: 6px 12px;
- background: var(--accent);
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.875rem;
- font-weight: 500;
- transition: all 150ms ease;
- white-space: nowrap;
-}
-
-.config-btn-add:hover {
- background: color-mix(in srgb, var(--accent) 85%, black);
- transform: translateY(-1px);
-}
-
-.config-btn-add:active {
- transform: translateY(0);
-}
-
-/* ---------------------------------------------------------------------------
- Utility — hidden class
- --------------------------------------------------------------------------- */
-.hidden {
- display: none !important;
-}
-
-/* ---------------------------------------------------------------------------
- Login Screen
- --------------------------------------------------------------------------- */
-.login-screen {
- position: fixed;
- inset: 0;
- z-index: 10001;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #090e17 0%, #101827 50%, #1b2838 100%);
-}
-.login-card {
- width: 380px;
- max-width: 90vw;
- background: rgba(30, 30, 46, 0.85);
- backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 16px;
- padding: 40px 36px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
- animation: loginFadeIn 0.4s ease-out;
-}
-@keyframes loginFadeIn {
- from {
- opacity: 0;
- transform: translateY(20px) scale(0.97);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-.login-header {
- text-align: center;
- margin-bottom: 32px;
-}
-.login-logo {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- margin-bottom: 8px;
-}
-.login-logo-icon {
- font-size: 2rem;
-}
-.login-logo-text {
- font-size: 1.8rem;
- font-weight: 700;
- background: linear-gradient(135deg, #7dd3fc, #38bdf8, #0ea5e9);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- letter-spacing: -0.02em;
-}
-.login-subtitle {
- font-size: 0.88rem;
- color: rgba(255, 255, 255, 0.45);
- margin: 0;
-}
-.login-form .form-group {
- margin-bottom: 20px;
-}
-.login-form .form-group label {
- display: block;
- font-size: 0.82rem;
- font-weight: 500;
- color: rgba(255, 255, 255, 0.6);
- margin-bottom: 6px;
-}
-.login-form input[type="text"],
-.login-form input[type="password"] {
- width: 100%;
- padding: 12px 14px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 10px;
- background: rgba(255, 255, 255, 0.06);
- color: #e2e8f0;
- font-size: 0.92rem;
- font-family: inherit;
- outline: none;
- transition:
- border-color 200ms,
- box-shadow 200ms;
- box-sizing: border-box;
-}
-.login-form input:focus {
- border-color: #818cf8;
- box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2);
-}
-.login-form input::placeholder {
- color: rgba(255, 255, 255, 0.25);
-}
-.password-input-wrapper {
- position: relative;
-}
-.password-toggle {
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1rem;
- opacity: 0.5;
- transition: opacity 150ms;
- padding: 4px;
-}
-.password-toggle:hover {
- opacity: 0.9;
-}
-.form-group--inline {
- display: flex;
- align-items: center;
-}
-.checkbox-label {
- display: flex;
- align-items: center;
- gap: 8px;
- cursor: pointer;
- font-size: 0.82rem;
- color: rgba(255, 255, 255, 0.5);
-}
-.checkbox-label input[type="checkbox"] {
- accent-color: #818cf8;
- width: 15px;
- height: 15px;
-}
-.login-error {
- padding: 10px 14px;
- border-radius: 8px;
- background: rgba(239, 68, 68, 0.12);
- border: 1px solid rgba(239, 68, 68, 0.3);
- color: #fca5a5;
- font-size: 0.82rem;
- margin-bottom: 16px;
-}
-.btn-login {
- width: 100%;
- padding: 13px 24px;
- border: none;
- border-radius: 10px;
- background: linear-gradient(135deg, #3b82f6, #2563eb);
- color: #fff;
- font-size: 0.95rem;
- font-weight: 600;
- cursor: pointer;
- transition:
- opacity 200ms,
- transform 100ms;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- font-family: inherit;
-}
-.btn-login:hover:not(:disabled) {
- opacity: 0.92;
-}
-.btn-login:active:not(:disabled) {
- transform: scale(0.98);
-}
-.btn-login:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-.btn-spinner {
- font-size: 1rem;
-}
-
-/* Light theme login */
-[data-theme="light"] .login-screen {
- background: linear-gradient(135deg, #dbeafe, #eff6ff, #bfdbfe);
-}
-[data-theme="light"] .login-card {
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid rgba(0, 0, 0, 0.08);
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
-}
-[data-theme="light"] .login-subtitle {
- color: rgba(0, 0, 0, 0.45);
-}
-[data-theme="light"] .login-form .form-group label {
- color: rgba(0, 0, 0, 0.6);
-}
-[data-theme="light"] .login-form input[type="text"],
-[data-theme="light"] .login-form input[type="password"] {
- background: rgba(0, 0, 0, 0.04);
- border-color: rgba(0, 0, 0, 0.12);
- color: #1e293b;
-}
-[data-theme="light"] .login-form input::placeholder {
- color: rgba(0, 0, 0, 0.3);
-}
-[data-theme="light"] .checkbox-label {
- color: rgba(0, 0, 0, 0.5);
-}
-[data-theme="light"] .login-error {
- background: rgba(239, 68, 68, 0.08);
- color: #dc2626;
-}
-
-/* ---------------------------------------------------------------------------
- User Menu (header)
- --------------------------------------------------------------------------- */
-.user-menu {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.user-display-name {
- font-size: 0.8rem;
- color: var(--text-secondary);
- font-weight: 500;
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.user-menu-link,
-.btn-logout {
- display: flex;
- align-items: center;
- justify-content: center;
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- transition:
- color 150ms,
- background 150ms;
-}
-.user-menu-link:hover,
-.btn-logout:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-.btn-logout:hover {
- color: var(--danger);
-}
-
-/* ---------------------------------------------------------------------------
- Admin Panel
- --------------------------------------------------------------------------- */
-.admin-toolbar {
- padding: 16px;
- display: flex;
- justify-content: flex-end;
-}
-.admin-users-list {
- padding: 0 16px 16px;
-}
-.admin-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.82rem;
-}
-.admin-table th,
-.admin-table td {
- padding: 10px 12px;
- text-align: left;
- border-bottom: 1px solid var(--border);
-}
-.admin-table th {
- font-size: 0.72rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- font-weight: 600;
- background: var(--bg-primary);
- position: sticky;
- top: 0;
-}
-.admin-table td {
- color: var(--text-primary);
-}
-.admin-role-badge {
- display: inline-block;
- padding: 2px 10px;
- border-radius: 10px;
- font-size: 0.72rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.03em;
-}
-.admin-role-admin {
- background: rgba(59, 130, 246, 0.15);
- color: #3b82f6;
-}
-.admin-role-user {
- background: rgba(34, 197, 94, 0.12);
- color: #22c55e;
-}
-.admin-vaults-text {
- font-size: 0.78rem;
- color: var(--text-secondary);
-}
-.admin-actions {
- white-space: nowrap;
-}
-.admin-action-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px 6px;
- border-radius: 4px;
- font-size: 0.9rem;
- transition: background 150ms;
-}
-.admin-action-btn:hover {
- background: var(--bg-hover);
-}
-.admin-action-btn.danger:hover {
- background: rgba(239, 68, 68, 0.12);
-}
-
-/* Admin form overlay */
-.admin-form-overlay {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- backdrop-filter: blur(4px);
-}
-.admin-form-card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 28px 24px;
- width: 440px;
- max-width: 90vw;
- max-height: 80vh;
- overflow-y: auto;
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
-}
-.admin-form-card h3 {
- margin: 0 0 20px;
- font-size: 1.05rem;
- color: var(--text-primary);
-}
-.admin-form-card .form-group {
- margin-bottom: 16px;
-}
-.admin-form-card .form-group label {
- display: block;
- font-size: 0.82rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 6px;
-}
-.admin-form-card input[type="text"],
-.admin-form-card input[type="password"],
-.admin-form-card select {
- width: 100%;
- padding: 10px 12px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 0.88rem;
- font-family: inherit;
- outline: none;
- transition: border-color 200ms;
- box-sizing: border-box;
-}
-.admin-form-card input:focus,
-.admin-form-card select:focus {
- border-color: var(--accent);
-}
-.admin-vault-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- max-height: 160px;
- overflow-y: auto;
- padding: 8px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: 8px;
-}
-.admin-vault-list .checkbox-label {
- color: var(--text-primary);
- font-size: 0.82rem;
-}
-.admin-form-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- margin-top: 20px;
-}
-
-/* ---------------------------------------------------------------------------
- Popup Mode (Nouvelle fenêtre)
- --------------------------------------------------------------------------- */
-body.popup-mode .top-nav,
-body.popup-mode .sidebar,
-body.popup-mode .resizer,
-body.popup-mode .content-header {
- display: none !important;
-}
-body.popup-mode .main-layout {
- height: 100vh;
-}
-body.popup-mode .content-area {
- margin: 15px auto;
- padding: 30px;
- border: 1px solid var(--border);
- border-radius: 12px;
- height: calc(100vh - 30px);
- max-width: 1000px;
- overflow-y: auto;
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
- box-sizing: border-box;
-}
-
-/* ---------------------------------------------------------------------------
- Recent Tab Styles
- --------------------------------------------------------------------------- */
-
-/* Liste récente */
-.recent-list {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 4px 0;
-}
-
-/* Item */
-.recent-item {
- padding: 10px 12px;
- border-radius: 6px;
- cursor: pointer;
- border-left: 3px solid transparent;
- transition:
- background 0.15s,
- border-color 0.15s;
-}
-.recent-item:hover {
- background: var(--bg-secondary);
- border-left-color: var(--accent);
-}
-.recent-item.active {
- border-left-color: var(--accent);
- background: var(--bg-secondary);
-}
-
-/* Header: temps + badge vault */
-.recent-item-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 3px;
-}
-.recent-time {
- font-size: 11px;
- color: var(--text-muted);
- display: flex;
- align-items: center;
- gap: 4px;
-}
-.recent-vault-badge {
- font-size: 10px;
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 10px;
- background: var(--accent);
- color: #fff;
- opacity: 0.85;
-}
-
-/* Titre */
-.recent-item-title {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 2px;
-}
-
-/* Breadcrumb path */
-.recent-item-path {
- font-size: 11px;
- color: var(--text-muted);
- margin-bottom: 4px;
- font-family: monospace;
-}
-
-/* Preview */
-.recent-item-preview {
- font-size: 12px;
- color: var(--text-muted);
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- line-height: 1.4;
- margin-bottom: 5px;
-}
-
-/* Tags */
-.recent-item-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
-}
-.recent-item-tags .tag-pill {
- font-size: 10px;
- padding: 1px 6px;
- border-radius: 10px;
- background: var(--bg-secondary);
- color: var(--accent);
- border: 1px solid var(--border);
-}
-
-/* Filtre vault */
-.recent-filter-bar {
- padding: 8px 12px 4px;
-}
-.recent-filter-bar select {
- width: 100%;
- padding: 4px 8px;
- font-size: 12px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border);
- border-radius: 4px;
- cursor: pointer;
-}
-
-/* État vide */
-.recent-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 32px 16px;
- color: var(--text-muted);
- gap: 8px;
- font-size: 13px;
-}
-
-/* ---------------------------------------------------------------------------
- Find in Page
- --------------------------------------------------------------------------- */
-
-/* Animation d'entrée */
-@keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* Barre principale */
-.find-in-page-bar {
- position: fixed;
- bottom: 20px;
- right: 20px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 12px 16px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- z-index: 1000;
- animation: slideUp 200ms ease;
- max-width: calc(100vw - 40px);
-}
-
-.find-in-page-bar[hidden] {
- display: none;
-}
-
-/* Contenu de la barre */
-.find-in-page-content {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
-}
-
-/* Icône de recherche */
-.find-icon {
- color: var(--text-secondary);
- flex-shrink: 0;
-}
-
-/* Input de recherche */
-#find-input {
- flex: 1;
- min-width: 200px;
- padding: 6px 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.85rem;
- outline: none;
- transition: border-color 200ms ease;
-}
-
-#find-input:focus {
- border-color: var(--accent);
-}
-
-/* Compteur d'occurrences */
-.find-counter {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.75rem;
- color: var(--text-secondary);
- white-space: nowrap;
- min-width: 90px;
- text-align: center;
-}
-
-/* Boutons de navigation */
-.find-nav-buttons {
- display: flex;
- gap: 4px;
-}
-
-/* Boutons génériques */
-.find-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 150ms ease;
- padding: 0;
- flex-shrink: 0;
-}
-
-.find-btn:hover:not(:disabled) {
- border-color: var(--accent);
- color: var(--accent);
- background: color-mix(in srgb, var(--accent) 10%, var(--bg-primary));
-}
-
-.find-btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-/* Options */
-.find-options {
- display: flex;
- gap: 4px;
- border-left: 1px solid var(--border);
- padding-left: 10px;
-}
-
-.find-option-btn {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.7rem;
- font-weight: 600;
-}
-
-.find-option-btn[aria-pressed="true"] {
- border-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 20%, var(--bg-primary));
- color: var(--accent);
-}
-
-.find-option-text {
- pointer-events: none;
-}
-
-/* Bouton fermer */
-.find-close {
- border-left: 1px solid var(--border);
- margin-left: 6px;
-}
-
-.find-close:hover {
- background: var(--danger-bg);
- border-color: var(--danger);
- color: var(--danger);
-}
-
-/* Message d'erreur */
-.find-error {
- margin-top: 8px;
- padding: 6px 10px;
- background: var(--danger-bg);
- border: 1px solid var(--danger);
- border-radius: 6px;
- color: var(--danger);
- font-size: 0.75rem;
- font-family: "JetBrains Mono", monospace;
-}
-
-.find-error[hidden] {
- display: none;
-}
-
-/* Highlights dans le contenu */
-.find-highlight {
- background: rgba(255, 235, 59, 0.4);
- padding: 0 2px;
- border-radius: 2px;
- transition: background 150ms ease;
-}
-
-[data-theme="dark"] .find-highlight {
- background: rgba(255, 193, 7, 0.35);
-}
-
-.find-highlight-active {
- background: rgba(255, 193, 7, 0.7);
- box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.3);
-}
-
-[data-theme="dark"] .find-highlight-active {
- background: rgba(255, 193, 7, 0.6);
- box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.4);
-}
-
-/* Responsive mobile */
-@media (max-width: 768px) {
- .find-in-page-bar {
- bottom: 10px;
- left: 10px;
- right: 10px;
- max-width: none;
- padding: 10px 12px;
- }
-
- .find-in-page-content {
- gap: 6px;
- }
-
- #find-input {
- min-width: 120px;
- font-size: 0.8rem;
- }
-
- .find-counter {
- font-size: 0.7rem;
- min-width: 80px;
- }
-
- .find-btn {
- width: 32px;
- height: 32px;
- }
-
- .find-options {
- padding-left: 6px;
- }
-}
-
-/* ===== PWA STYLES ===== */
-
-/* PWA Update Notification */
-.pwa-update-notification {
- position: fixed;
- bottom: 20px;
- right: 20px;
- z-index: 10000;
- animation: slideInUp 0.3s ease-out;
-}
-
-.pwa-update-content {
- background: var(--accent);
- color: white;
- padding: 16px 20px;
- border-radius: var(--radius);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- display: flex;
- align-items: center;
- gap: 12px;
- max-width: 400px;
-}
-
-.pwa-update-content span {
- flex: 1;
- font-size: 0.9rem;
- font-weight: 500;
-}
-
-.pwa-update-btn {
- background: white;
- color: var(--accent);
- border: none;
- padding: 8px 16px;
- border-radius: var(--radius);
- font-size: 0.85rem;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s ease;
- white-space: nowrap;
-}
-
-.pwa-update-btn:hover {
- transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-}
-
-.pwa-update-dismiss {
- background: transparent;
- border: none;
- color: white;
- font-size: 1.5rem;
- line-height: 1;
- cursor: pointer;
- padding: 0;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0.8;
- transition: opacity 0.2s ease;
-}
-
-.pwa-update-dismiss:hover {
- opacity: 1;
-}
-
-/* PWA Install Button (optional - can be added to header) */
-#pwa-install-btn {
- display: none;
- background: var(--accent);
- color: white;
- border: none;
- padding: 8px 16px;
- border-radius: var(--radius);
- font-size: 0.85rem;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-#pwa-install-btn:hover {
- background: var(--accent-green);
- transform: translateY(-1px);
-}
-
-/* Animations */
-@keyframes slideInUp {
- from {
- transform: translateY(100%);
- opacity: 0;
- }
- to {
- transform: translateY(0);
- opacity: 1;
- }
-}
-
-/* Mobile responsive */
-@media (max-width: 768px) {
- .pwa-update-notification {
- bottom: 10px;
- right: 10px;
- left: 10px;
- }
-
- .pwa-update-content {
- padding: 12px 16px;
- font-size: 0.85rem;
- }
-
- .pwa-update-btn {
- padding: 6px 12px;
- font-size: 0.8rem;
- }
-}
-
-/* ---------------------------------------------------------------------------
- Dashboard Home - Recent Files Widget
- --------------------------------------------------------------------------- */
-
-.dashboard-home {
- padding: 0;
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-.dashboard-section {
- display: flex;
- flex-direction: column;
-}
-
-.dashboard-header {
- margin-bottom: 20px;
-}
-
-.dashboard-title-row {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 16px;
-}
-
-.dashboard-icon {
- width: 28px;
- height: 28px;
- color: var(--accent);
-}
-
-.dashboard-title-row h2 {
- font-family: "JetBrains Mono", monospace;
- font-size: 1.4rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.dashboard-filter-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.dashboard-select {
- padding: 6px 12px;
- font-size: 13px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border);
- border-radius: 6px;
- cursor: pointer;
- min-width: 160px;
-}
-
-.dashboard-select:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.dashboard-count {
- font-size: 12px;
- color: var(--text-muted);
- padding: 4px 10px;
- background: var(--bg-secondary);
- border-radius: 12px;
-}
-
-/* Grid */
-.dashboard-recent-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 16px;
- flex: 1;
- width: 100%;
-}
-
-@media (max-width: 1200px) {
- .dashboard-recent-grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (max-width: 768px) {
- .dashboard-home {
- padding: 16px;
- }
-
- .dashboard-recent-grid {
- grid-template-columns: 1fr;
- }
-
- .dashboard-title-row h2 {
- font-size: 1.2rem;
- }
-}
-
-/* Card */
-.dashboard-card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 12px;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
- display: flex;
- flex-direction: column;
- gap: 4px;
- animation: fadeSlideIn 0.3s ease forwards;
- opacity: 0;
-}
-
-.dashboard-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
- border-color: var(--accent);
-}
-
-.dashboard-card:active {
- transform: translateY(0);
-}
-
-@keyframes fadeSlideIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* Staggered animation for cards */
-.dashboard-card:nth-child(1) {
- animation-delay: 0ms;
-}
-.dashboard-card:nth-child(2) {
- animation-delay: 50ms;
-}
-.dashboard-card:nth-child(3) {
- animation-delay: 100ms;
-}
-.dashboard-card:nth-child(4) {
- animation-delay: 150ms;
-}
-.dashboard-card:nth-child(5) {
- animation-delay: 200ms;
-}
-.dashboard-card:nth-child(6) {
- animation-delay: 250ms;
-}
-.dashboard-card:nth-child(7) {
- animation-delay: 300ms;
-}
-.dashboard-card:nth-child(8) {
- animation-delay: 350ms;
-}
-.dashboard-card:nth-child(9) {
- animation-delay: 400ms;
-}
-
-/* Card header */
-.dashboard-card-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 8px;
-}
-
-.dashboard-card-icon {
- width: 40px;
- height: 40px;
- padding: 4px;
- background: transparent;
- border: 2px solid var(--accent);
- border-radius: 8px;
- color: var(--accent);
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.dashboard-card-icon svg {
- width: 24px;
- height: 24px;
-}
-
-.dashboard-vault-badge {
- font-size: 10px;
- font-weight: 600;
- padding: 3px 8px;
- border-radius: 10px;
- background: var(--bg-primary);
- color: var(--text-secondary);
- border: 1px solid var(--border);
- margin-right: auto;
-}
-
-.dashboard-card-bookmark-btn {
- background: transparent;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
-}
-
-.dashboard-card-bookmark-btn:hover {
- background: var(--bg-primary);
- color: var(--accent);
-}
-
-.dashboard-card-bookmark-btn.active {
- color: var(--accent-green);
-}
-
-/* Card title */
-.dashboard-card-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
- line-height: 1.4;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
-}
-
-/* Card path */
-.dashboard-card-path {
- font-size: 11px;
- color: var(--text-muted);
- font-family: "JetBrains Mono", monospace;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Card preview */
-.dashboard-card-preview {
- display: none;
-}
-
-/* Card footer */
-.dashboard-card-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- margin-top: 4px;
- padding-top: 6px;
- border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
-}
-
-.dashboard-card-time {
- font-size: 11px;
- color: var(--text-muted);
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.dashboard-card-time svg {
- width: 12px;
- height: 12px;
-}
-
-/* Card tags */
-.dashboard-card-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
-}
-
-.dashboard-card-tags .tag-pill {
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 8px;
- background: var(--bg-tertiary);
- color: var(--accent);
- border: 1px solid var(--border);
-}
-
-/* Empty state */
-.dashboard-recent-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 48px 24px;
- color: var(--text-muted);
- gap: 12px;
- text-align: center;
- flex: 1;
-}
-
-.dashboard-recent-empty svg {
- width: 48px;
- height: 48px;
- opacity: 0.4;
-}
-
-.dashboard-recent-empty span {
- font-size: 16px;
- font-weight: 500;
-}
-
-.dashboard-recent-empty p {
- font-size: 13px;
- margin: 0;
- opacity: 0.7;
-}
-
-/* Loading skeletons */
-.dashboard-loading {
- display: none;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
-}
-
-.dashboard-loading.active {
- display: grid;
-}
-
-@media (max-width: 1200px) {
- .dashboard-loading {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (max-width: 768px) {
- .dashboard-loading {
- grid-template-columns: 1fr;
- }
-}
-
-.skeleton-card {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 16px;
- height: 160px;
- position: relative;
- overflow: hidden;
-}
-
-.skeleton-card::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
- animation: shimmer 1.5s infinite;
-}
-
-@keyframes shimmer {
- 0% {
- transform: translateX(-100%);
- }
- 100% {
- transform: translateX(100%);
- }
-}
-
-/* ===== CONTEXT MENU ===== */
-.context-menu {
- position: fixed;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
- min-width: 200px;
- z-index: 10000;
- padding: 4px;
- display: none;
-}
-
-.context-menu.active {
- display: block;
-}
-
-.context-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 12px;
- border-radius: 6px;
- cursor: pointer;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.82rem;
- color: var(--text-primary);
- transition: all 150ms ease;
-}
-
-.context-menu-item:hover:not(.disabled) {
- background: var(--bg-hover);
- color: var(--accent);
-}
-
-.context-menu-item.disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.context-menu-item .icon {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
-}
-
-.context-menu-separator {
- height: 1px;
- background: var(--border);
- margin: 4px 0;
-}
-
-/* ===== INLINE RENAME ===== */
-.sidebar-item-input {
- width: 100%;
- padding: 4px 8px;
- background: var(--bg-primary);
- border: 1px solid var(--accent);
- border-radius: 4px;
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.82rem;
- outline: none;
-}
-
-/* ===== VAULT READONLY BADGE ===== */
-.vault-readonly-badge {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 6px;
- background: var(--danger-bg);
- color: var(--danger);
- border-radius: 4px;
- font-size: 0.7rem;
- font-weight: 600;
- margin-left: 6px;
-}
-
-/* ===== OBSIGATE MODALS ===== */
-.obsigate-modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: var(--overlay-bg);
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- pointer-events: none;
- transition: opacity 200ms ease;
-}
-
-.obsigate-modal-overlay.active {
- opacity: 1;
- pointer-events: all;
-}
-
-.obsigate-modal {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 12px;
- box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
- min-width: 400px;
- max-width: 600px;
- transform: scale(0.9);
- transition: transform 200ms ease;
-}
-
-.obsigate-modal-overlay.active .obsigate-modal {
- transform: scale(1);
-}
-
-.obsigate-modal-header {
- padding: 16px 20px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.obsigate-modal-title {
- font-family: "JetBrains Mono", monospace;
- font-size: 1.1rem;
- font-weight: 700;
- color: var(--text-primary);
-}
-
-.obsigate-modal-body {
- padding: 20px;
-}
-
-.obsigate-modal-footer {
- padding: 16px 20px;
- border-top: 1px solid var(--border);
- display: flex;
- gap: 10px;
- justify-content: flex-end;
-}
-
-.modal-input {
- width: 100%;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: 6px;
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.9rem;
- outline: none;
- transition: border-color 200ms ease;
-}
-
-.modal-input:focus {
- border-color: var(--accent);
-}
-
-.modal-input.error {
- border-color: var(--danger);
-}
-
-.modal-label {
- display: block;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.85rem;
- font-weight: 600;
- color: var(--text-secondary);
- margin-bottom: 8px;
-}
-
-.modal-hint {
- font-size: 0.75rem;
- color: var(--text-muted);
- margin-top: 6px;
-}
-
-.modal-error {
- font-size: 0.8rem;
- color: var(--danger);
- margin-top: 6px;
-}
-
-.modal-select {
- width: 100%;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: 6px;
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.9rem;
- outline: none;
- cursor: pointer;
- transition: border-color 200ms ease;
-}
-
-.modal-select:focus {
- border-color: var(--accent);
-}
-
-.modal-btn {
- padding: 8px 16px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-family: "JetBrains Mono", monospace;
- font-size: 0.85rem;
- cursor: pointer;
- transition: all 150ms ease;
-}
-
-.modal-btn:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-.modal-btn.primary {
- background: var(--accent);
- color: white;
- border-color: var(--accent);
-}
-
-.modal-btn.primary:hover {
- opacity: 0.9;
-}
-
-.modal-btn.danger {
- background: var(--danger);
- color: white;
- border-color: var(--danger);
-}
-
-.modal-btn.danger:hover {
- opacity: 0.9;
-}
-
-.modal-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.modal-form-group {
- margin-bottom: 16px;
-}
-
-.modal-form-group:last-child {
- margin-bottom: 0;
-}
-
-.modal-warning {
- padding: 12px;
- background: var(--danger-bg);
- border: 1px solid var(--danger);
- border-radius: 6px;
- color: var(--danger);
- font-size: 0.85rem;
- margin-bottom: 16px;
- display: flex;
- align-items: flex-start;
- gap: 10px;
-}
-
-.modal-warning .icon {
- flex-shrink: 0;
- margin-top: 2px;
-}
-
-/* ===== TAB BAR ===== */
-.tab-bar {
- display: flex;
- align-items: stretch;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border);
- overflow: hidden;
- flex-shrink: 0;
- min-height: 36px;
-}
-
-.tab-bar[hidden] {
- display: none;
-}
-
-.tab-list {
- display: flex;
- align-items: stretch;
- overflow-x: auto;
- overflow-y: hidden;
- flex: 1;
- scrollbar-width: thin;
-}
-
-.tab-list::-webkit-scrollbar {
- height: 3px;
-}
-
-.tab-item {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- font-size: 0.8rem;
- color: var(--text-muted);
- cursor: pointer;
- white-space: nowrap;
- border-right: 1px solid var(--border);
- background: transparent;
- transition: background 0.15s, color 0.15s;
- user-select: none;
- position: relative;
- min-width: 0;
- flex-shrink: 0;
-}
-
-.tab-item:hover {
- background: var(--bg-hover);
- color: var(--text-secondary);
-}
-
-.tab-item.active {
- background: var(--bg-primary);
- color: var(--text-primary);
- border-bottom: 2px solid var(--accent);
- margin-bottom: -1px;
-}
-
-.tab-item .tab-icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- opacity: 0.7;
-}
-
-.tab-item .tab-name {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 150px;
-}
-
-/* Preview tab (single-click) — italic title, slightly muted */
-.tab-item.preview .tab-name {
- font-style: italic;
- opacity: 0.75;
-}
-.tab-item.preview {
- background: var(--bg-hover);
-}
-
-.tab-item .tab-close {
- width: 16px;
- height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 3px;
- flex-shrink: 0;
- opacity: 0;
- transition: opacity 0.15s, background 0.15s;
-}
-
-.tab-item:hover .tab-close,
-.tab-item.active .tab-close {
- opacity: 0.6;
-}
-
-.tab-item .tab-close:hover {
- opacity: 1;
- background: var(--bg-hover);
-}
-
-.tab-item.dragging {
- opacity: 0.5;
- background: var(--bg-hover);
-}
-
-.tab-drop-indicator {
- width: 2px;
- background: var(--accent);
- flex-shrink: 0;
-}
-
-/* ── Backlinks panel ── */
-.backlinks-panel {
- margin-top: 24px;
- padding: 12px 16px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 8px;
- border: 1px solid var(--border);
-}
-.backlinks-header {
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-muted);
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 8px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-.backlinks-list {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-.backlink-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 6px 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: background 0.15s;
-}
-.backlink-item:hover {
- background: var(--bg-hover);
-}
-.backlink-vault {
- font-size: 0.7rem;
- color: var(--accent);
- background: var(--accent-bg, rgba(99, 102, 241, 0.1));
- padding: 1px 6px;
- border-radius: 3px;
-}
-.backlink-title {
- font-size: 0.85rem;
- color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* ── Unsupported file notice ── */
-.unsupported-file {
- padding: 40px;
- text-align: center;
- color: var(--text-muted);
-}
-.unsupported-file i {
- display: block;
- margin: 0 auto 12px;
- opacity: 0.4;
-}
-.unsupported-file .filename {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
- word-break: break-all;
-}
-.unsupported-file .btn-action {
- margin-top: 12px;
- display: inline-flex;
-}
-
-/* ── Dashboard Stats Grid ── */
-.dashboard-stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
- gap: 12px;
-}
-.stat-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 16px 12px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 8px;
- border: 1px solid var(--border);
-}
-.stat-icon {
- width: 24px;
- height: 24px;
- opacity: 0.6;
- margin-bottom: 8px;
- color: var(--accent);
-}
-.stat-value {
- font-size: 1.4rem;
- font-weight: 700;
- color: var(--text-primary);
-}
-.stat-label {
- font-size: 0.75rem;
- color: var(--text-muted);
- margin-top: 2px;
-}
-.dashboard-stats-loading {
- padding: 16px;
- text-align: center;
- color: var(--text-muted);
-}
-
-/* ── Dashboard Conflicts ── */
-.dashboard-conflicts-grid {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-.conflict-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 14px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 6px;
- border: 1px solid var(--border);
- gap: 12px;
-}
-.conflict-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-}
-.conflict-vault {
- font-size: 0.7rem;
- color: var(--accent);
- background: var(--accent-bg, rgba(99,102,241,0.1));
- padding: 1px 6px;
- border-radius: 3px;
- align-self: flex-start;
-}
-.conflict-name {
- font-size: 0.85rem;
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.conflict-date {
- font-size: 0.7rem;
- color: var(--text-muted);
-}
-.conflict-actions {
- display: flex;
- gap: 8px;
- flex-shrink: 0;
-}
-.conflict-btn {
- padding: 4px 10px;
- font-size: 0.75rem;
- border: 1px solid var(--border);
- border-radius: 4px;
- cursor: pointer;
- background: var(--bg-primary);
- color: var(--text-primary);
- white-space: nowrap;
-}
-.conflict-btn:hover { background: var(--bg-hover); }
-
-/* ── Quick Help (Dashboard) ── */
-.quick-help {
- margin-top: 16px;
- background: var(--bg-card, var(--bg-secondary));
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- overflow: hidden;
-}
-.qh-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 14px;
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-secondary);
- border-bottom: 1px solid var(--border);
- user-select: none;
-}
-.qh-header i { color: var(--accent); opacity: 0.7; }
-.qh-toggle {
- margin-left: auto;
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- font-size: 1rem;
- line-height: 1;
- padding: 2px 6px;
- border-radius: 4px;
-}
-.qh-toggle:hover { color: var(--text-secondary); background: var(--bg-hover); }
-.qh-body {
- padding: 10px 14px 14px;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- gap: 6px 20px;
-}
-.qh-body.collapsed { display: none; }
-.qh-row {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 4px 0;
-}
-.qh-kbd {
- display: inline-flex;
- align-items: center;
- padding: 2px 8px;
- font-family: var(--mono);
- font-size: 0.7rem;
- font-weight: 500;
- color: var(--accent-text, #79c0ff);
- background: var(--accent-bg, #1f2a3a);
- border: 1px solid var(--border-md, #30363d);
- border-radius: 4px;
- white-space: nowrap;
- min-width: 0;
- letter-spacing: 0.3px;
-}
-.qh-desc {
- font-size: 0.78rem;
- color: var(--text-secondary);
-}
-.qh-divider {
- grid-column: 1 / -1;
- height: 1px;
- background: var(--border);
- margin: 6px 0 8px;
-}
-.qh-tip {
- grid-column: 1 / -1;
- display: flex;
- align-items: flex-start;
- gap: 8px;
- font-size: 0.75rem;
- color: var(--text-muted);
- line-height: 1.5;
-}
-.qh-tip i { color: var(--warn); flex-shrink: 0; margin-top: 1px; }
-.qh-tip strong { color: var(--text-secondary); }
-
-/* ── Webhooks UI ── */
-.webhook-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 6px;
- margin-bottom: 6px;
- font-size: 0.8rem;
-}
-.webhook-name { font-weight: 500; min-width: 80px; }
-.webhook-url { color: var(--accent); overflow: hidden; text-overflow: ellipsis; flex: 1; }
-.webhook-events { color: var(--text-muted); font-size: 0.7rem; }
-.webhook-delete { background: none; border: none; color: var(--text-error); cursor: pointer; font-size: 1rem; padding: 2px 6px; }
-.config-add-row { display: flex; gap: 8px; margin-top: 8px; }
-.config-btn-add { padding: 6px 14px; background: var(--accent); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.8rem; }
-
-/* ── Shares UI ── */
-.share-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 6px;
- margin-bottom: 6px;
- font-size: 0.8rem;
-}
-.share-path { font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
-.share-url a { color: var(--accent); font-size: 0.75rem; }
-.share-meta { color: var(--text-muted); font-size: 0.7rem; }
-.share-revoke { background: none; border: 1px solid var(--text-error); color: var(--text-error); cursor: pointer; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; }
-
-/* ── Share Dialog ── */
-.share-dialog-overlay {
- position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(0,0,0,0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10000;
-}
-.share-dialog {
- background: var(--bg-primary);
- border-radius: 12px;
- padding: 24px;
- max-width: 480px;
- width: 90%;
- box-shadow: 0 8px 32px rgba(0,0,0,0.2);
-}
-.share-dialog h3 { margin: 0 0 4px; }
-.share-url-input {
- width: 100%;
- padding: 8px 12px;
- border: 1px solid var(--border);
- border-radius: 6px;
- font-size: 0.8rem;
- background: var(--bg-secondary);
- color: var(--text-primary);
- box-sizing: border-box;
-}
-.share-dialog-actions {
- display: flex;
- gap: 8px;
- margin-top: 12px;
- justify-content: flex-end;
-}
-.share-copy-btn {
- padding: 6px 16px;
- background: var(--accent);
- color: #fff;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.85rem;
-}
-.share-close-btn {
- padding: 6px 16px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border);
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.85rem;
-}
-.share-create-btn {
- padding: 6px 16px;
- background: var(--accent);
- color: #fff;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.85rem;
-}
-.share-revoke-btn {
- padding: 6px 16px;
- background: none;
- color: var(--text-error);
- border: 1px solid var(--text-error);
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.85rem;
-}
-
-/* ── Enhanced Search Bar Toggles ── */
-.search-actions .tog {
- font-family: var(--font-mono, monospace);
- font-size: 10px;
- font-weight: 600;
- padding: 2px 5px;
- border-radius: 3px;
- min-width: auto;
- line-height: 1.4;
-}
-.search-actions .tog.active {
- color: var(--accent);
- border-color: var(--accent);
- background: rgba(99, 102, 241, 0.12);
-}
-.search-actions .icon-only {
- padding: 2px 4px;
- min-width: auto;
-}
-.search-sep {
- width: 1px;
- height: 16px;
- background: var(--border);
- margin: 0 2px;
- align-self: center;
-}
-.search-count {
- font-size: 10px;
- color: var(--text-muted);
- font-family: var(--font-mono, monospace);
- white-space: nowrap;
- min-width: 28px;
- text-align: center;
-}
-.search-filter-row, .search-replace-row {
- display: flex;
- gap: 6px;
- align-items: center;
- padding: 4px 0 0 0;
-}
-.search-filter-input {
- flex: 1;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text-primary);
- font-family: var(--font-mono, monospace);
- font-size: 11px;
- padding: 4px 8px;
- outline: none;
-}
-.search-filter-input:focus { border-color: var(--accent); }
-.search-filter-input::placeholder { color: var(--text-muted); }
-
-/* ── Floating Results Bar ── */
-.results-bar {
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- align-items: center;
- gap: 2px;
- margin: 0 auto;
- padding: 6px 10px;
- background: var(--bg-secondary);
- border: 1px solid var(--border-md, var(--border));
- border-radius: 10px;
- box-shadow: 0 2px 16px rgba(0,0,0,0.35);
- width: fit-content;
- max-width: 95%;
- z-index: 50;
- backdrop-filter: blur(8px);
-}
-.results-bar-count {
- font-size: 10px;
- color: var(--text-muted);
- font-family: var(--mono);
- white-space: nowrap;
- min-width: 28px;
- text-align: center;
- padding: 0 4px;
-}
-.results-bar-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- border: 1px solid transparent;
- border-radius: 4px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- font-size: 12px;
- transition: background 0.15s, color 0.15s, border-color 0.15s;
-}
-.results-bar-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
-.results-bar-btn.tog {
- font-family: var(--mono);
- font-size: 10px;
- font-weight: 600;
- width: auto;
- padding: 2px 5px;
-}
-.results-bar-btn.tog.active {
- color: var(--accent);
- border-color: var(--accent);
- background: rgba(99,102,241,0.12);
-}
-.results-bar-sep {
- width: 1px;
- height: 16px;
- background: var(--border);
- margin: 0 6px;
- align-self: center;
-}
-.search-replace-btn {
- padding: 4px 10px;
- font-size: 11px;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- cursor: pointer;
- white-space: nowrap;
-}
-.search-replace-btn:hover { background: var(--bg-hover); }
-.search-replace-btn.destructive { border-color: var(--text-error); color: var(--text-error); }
-
-/* ── Active search result highlight ── */
-.btn-bookmark.active {
- color: #059669;
- border-color: #059669;
- background: rgba(5, 150, 105, 0.08);
-}
-.btn-bookmark.active:hover {
- background: rgba(5, 150, 105, 0.15);
-}
-
-.btn-share.shared {
- color: #059669;
- border-color: #059669;
- background: rgba(5, 150, 105, 0.08);
-}
-.btn-share.shared:hover {
- background: rgba(5, 150, 105, 0.15);
-}
-
-/* ── Shared Cards ── */
-.shared-card {
- padding: 10px 14px;
- background: var(--bg-card, var(--bg-secondary));
- border-radius: 6px;
- border: 1px solid var(--border);
- cursor: pointer;
- transition: background 0.15s;
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-.shared-card:hover { background: var(--bg-hover); }
-.shared-card-header {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.shared-card-title { font-weight: 500; font-size: 0.85rem; }
-.shared-card-vault {
- font-size: 0.65rem;
- color: var(--accent);
- background: var(--accent-bg, rgba(99,102,241,0.1));
- padding: 1px 6px;
- border-radius: 3px;
- margin-left: auto;
-}
-.shared-card-meta {
- font-size: 0.7rem;
- color: var(--text-muted);
- display: flex;
- gap: 12px;
-}
-.shared-card-actions {
- display: flex;
- gap: 6px;
-}
-.shared-card-actions button {
- font-size: 0.7rem;
- padding: 2px 8px;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--bg-primary);
- color: var(--text-primary);
- cursor: pointer;
-}
-.shared-card-actions button:hover { background: var(--bg-hover); }
-
-/* ── Dashboard Tab Navigation ── */
-.dashboard-tabs {
- display: flex;
- gap: 2px;
- margin-bottom: 16px;
- border-bottom: 2px solid var(--border);
-}
-.dashboard-tab {
- padding: 8px 16px;
- border: none;
- background: none;
- color: var(--text-muted);
- cursor: pointer;
- font-size: 0.85rem;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 6px;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
- transition: color 0.15s, border-color 0.15s;
-}
-.dashboard-tab:hover { color: var(--text-primary); }
-.dashboard-tab.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
-}
-.dashboard-panel {
- display: none;
-}
-.dashboard-panel.active {
- display: block;
-}
-
-/* ═══════════════════════════════════════════════════════════════════
- Command Palette (Ctrl+P)
- ═══════════════════════════════════════════════════════════════════ */
-
-.command-palette-overlay {
- position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
- background: 0 0;
- z-index: 10000;
- display: flex;
- justify-content: center;
- padding-top: 12vh;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.1s ease, background 0.1s ease;
-}
-
-.command-palette-overlay.active {
- opacity: 1;
- background: var(--overlay-bg);
- pointer-events: auto;
-}
-
-.command-palette {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 10px;
- box-shadow: 0 8px 40px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.03);
- width: 580px;
- max-width: calc(100vw - 40px);
- max-height: 60vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- animation: cp-slide-in 0.12s ease-out;
-}
-
-@keyframes cp-slide-in {
- from { transform: translateY(-12px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
-}
-
-.cp-header {
- display: flex;
- align-items: center;
- border-bottom: 1px solid var(--border);
- padding: 0 14px;
-}
-
-.cp-input {
- flex: 1;
- border: none;
- background: none;
- padding: 14px 0;
- font-size: 16px;
- color: var(--text-primary);
- outline: none;
- font-family: inherit;
-}
-
-.cp-input::placeholder {
- color: var(--text-muted);
-}
-
-.cp-hint {
- font-size: 11px;
- color: var(--text-muted);
- background: var(--bg-hover);
- padding: 3px 8px;
- border-radius: 4px;
- font-family: "JetBrains Mono", monospace;
- opacity: 0.7;
-}
-
-.cp-results {
- flex: 1;
- overflow-y: auto;
- padding: 6px 0;
- max-height: 40vh;
-}
-
-.cp-empty {
- padding: 24px 16px;
- text-align: center;
- color: var(--text-muted);
- font-size: 13px;
-}
-
-.cp-error {
- color: var(--accent-orange, #d29922);
-}
-
-.cp-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 14px;
- cursor: pointer;
- border-left: 3px solid transparent;
-}
-
-.cp-item:hover {
- background: var(--bg-hover);
- border-left-color: var(--accent);
-}
-
-.cp-item.cp-selected {
- background: var(--accent-bg);
- border-left-color: var(--accent);
-}
-
-.cp-item-label {
- font-size: 14px;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.cp-item-sublabel {
- font-size: 11px;
- color: var(--text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 45%;
- text-align: right;
- margin-left: 12px;
-}
-
-.cp-footer {
- display: flex;
- gap: 14px;
- padding: 8px 14px;
- border-top: 1px solid var(--border);
- font-size: 11px;
- color: var(--text-muted);
-}
-
-.cp-footer span {
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.cp-footer span::before {
- content: '';
- display: inline-block;
- width: 3px; height: 3px;
- background: var(--text-muted);
- border-radius: 50%;
- margin-right: 4px;
-}
-
-.cp-footer span:first-child::before { display: none; }
-
-/* ── Breadcrumb ── */
-.cp-breadcrumb {
- display: none;
- align-items: center;
- gap: 2px;
- padding: 8px 14px;
- font-size: 12px;
- color: var(--text-secondary);
- background: var(--bg-primary);
- border-bottom: 1px solid var(--border);
- overflow-x: auto;
- white-space: nowrap;
- scrollbar-width: none;
-}
-.cp-breadcrumb::-webkit-scrollbar { display: none; }
-.cp-bc-root { font-weight: 600; color: var(--accent); }
-.cp-bc-sep { color: var(--text-muted); padding: 0 2px; }
-.cp-bc-item { cursor: pointer; padding: 1px 4px; border-radius: 3px; }
-.cp-bc-item:hover { background: var(--bg-hover); }
-
-/* ── Name input ── */
-.cp-name-input {
- display: flex;
- flex-direction: column;
- padding: 8px 14px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-primary);
-}
-.cp-name-field {
- width: 100%;
- border: 1px solid var(--border);
- background: var(--bg-secondary);
- color: var(--text-primary);
- padding: 10px 12px;
- border-radius: 6px;
- font-size: 14px;
- font-family: inherit;
- outline: none;
- transition: border-color 0.15s;
-}
-.cp-name-field:focus { border-color: var(--accent); }
-.cp-name-hint {
- font-size: 11px;
- color: var(--text-muted);
- margin-top: 6px;
-}
-
-/* Browse filter in command palette */
-.cp-browse-filter {
- width: calc(100% - 24px);
- margin: 8px 12px 4px;
- padding: 7px 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 0.8rem;
- font-family: inherit;
- outline: none;
- transition: border-color 0.15s;
-}
-.cp-browse-filter:focus { border-color: var(--accent); }
-.cp-browse-filter::placeholder { color: var(--text-muted); }
-
-/* ── Help Modal: Mobile responsive ── */
-@media (max-width: 768px) {
- .help-container,
- .editor-container.help-container {
- max-width: 100vw;
- width: 100vw;
- height: 100vh;
- max-height: 100vh;
- border-radius: 0;
- box-sizing: border-box;
- }
-
- .help-hamburger {
- display: inline-flex;
- }
-
- /* TOC: collapsible block at top (not slide-over) */
- .help-nav {
- position: static;
- width: 100%;
- min-width: 0;
- max-width: 100%;
- max-height: none;
- height: auto;
- z-index: auto;
- transform: none;
- transition: none;
- border-right: none;
- border-bottom: 1px solid var(--border);
- box-shadow: none;
- flex-shrink: 0;
- display: none;
- box-sizing: border-box;
- }
- .help-nav-header {
- padding: 10px 16px 6px;
- }
-
- /* Content full-width */
- .help-body {
- flex-direction: column;
- overflow-y: auto;
- overflow-x: hidden;
- padding: 0;
- width: 100%;
- box-sizing: border-box;
- }
- .help-content {
- padding: 14px 14px 40px;
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- word-break: break-word;
- width: 100%;
- max-width: 100%;
- box-sizing: border-box;
- }
- .help-content pre, .help-content code, .help-content table {
- max-width: 100%;
- overflow-x: auto;
- }
- .help-content:focus { outline: none; }
- .help-hero {
- flex-direction: column;
- gap: 16px;
- min-width: 0;
- width: 100%;
- box-sizing: border-box;
- padding: 20px 16px;
- }
- .help-hero-copy {
- min-width: 0;
- width: 100%;
- }
- .help-badges {
- flex-wrap: wrap;
- gap: 6px;
- }
- .help-badge {
- white-space: normal;
- }
- .help-hero-visual { display: none; }
- .help-section {
- padding: 20px 0 16px;
- min-width: 0;
- width: 100%;
- box-sizing: border-box;
- }
- .help-nav-link {
- padding: 10px 12px;
- font-size: 0.88rem;
- }
-}
-
-/* ══════════════════════════════════════════
- Mobile Bottom Toolbar (Obsidian-style)
- ══════════════════════════════════════════ */
-.mobile-toolbar {
- display: none;
-}
-.vault-context-bar {
- display: none;
-}
-
-@media (max-width: 768px) {
- .mobile-toolbar {
- display: flex;
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- height: 64px;
- background: var(--bg-secondary);
- border-top: 1px solid var(--border);
- z-index: 900;
- align-items: center;
- justify-content: space-around;
- padding: 4px 4px;
- padding-bottom: calc(4px + env(safe-area-inset-bottom, 0));
- -webkit-backdrop-filter: blur(12px);
- backdrop-filter: blur(12px);
- }
-
- /* Push content up to avoid toolbar overlap */
- .main-layout {
- padding-bottom: 64px;
- padding-bottom: calc(64px + env(safe-area-inset-bottom, 0));
- }
- .editor-modal.active ~ .main-layout {
- padding-bottom: 0;
- }
-
- /* Enable pull-to-refresh on mobile — unblock the overscroll chain */
- html {
- overscroll-behavior-y: auto;
- }
- body {
- overflow-y: auto;
- overscroll-behavior-y: auto;
- height: 100vh;
- height: 100dvh;
- }
- .app-container {
- overflow-y: visible;
- }
- .main-layout {
- overflow-y: visible;
- }
- .content-wrapper {
- overflow: visible;
- }
- .content-area {
- overscroll-behavior-y: auto;
- }
-
- /* Hide desktop tab bar on mobile (bottom toolbar has its own tab button) */
- .tab-bar {
- display: none !important;
- }
-
- /* Vault context indicator — hidden (now in header) */
- .vault-context-bar {
- display: none !important;
- }
- .vault-context-pill {
- display: none;
- }
-
- .mt-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 3px;
- flex: 1;
- height: auto;
- min-height: 48px;
- padding: 4px 4px;
- border: none;
- border-radius: 8px;
- background: transparent;
- color: var(--text-muted);
- cursor: pointer;
- transition: color 0.15s, background 0.15s;
- position: relative;
- -webkit-tap-highlight-color: transparent;
- }
- .mt-btn:active {
- background: rgba(255,255,255,0.06);
- }
- .mt-btn svg {
- flex-shrink: 0;
- }
- .mt-label {
- font-size: 10px;
- font-weight: 500;
- line-height: 1;
- }
- .mt-btn-primary {
- color: var(--accent);
- }
- .mt-btn-primary .mt-label {
- color: var(--accent);
- }
-
- /* Tab count badge */
- .mt-badge {
- position: absolute;
- top: 2px;
- right: 50%;
- transform: translateX(18px);
- background: var(--accent);
- color: #fff;
- font-size: 9px;
- font-weight: 700;
- min-width: 16px;
- height: 16px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 4px;
- line-height: 1;
- }
-
- /* ── Tab Switcher Bottom Sheet ── */
- .mt-overlay {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.5);
- z-index: 950;
- opacity: 0;
- transition: opacity 0.25s;
- }
- .mt-overlay.visible {
- display: block;
- opacity: 1;
- }
-
- .mt-tab-switcher {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- max-height: 60vh;
- background: var(--bg-secondary);
- border-radius: 16px 16px 0 0;
- z-index: 960;
- transform: translateY(100%);
- transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
- display: flex;
- flex-direction: column;
- box-shadow: 0 -4px 24px rgba(0,0,0,0.4);
- padding-bottom: env(safe-area-inset-bottom, 0);
- }
- .mt-tab-switcher.open {
- transform: translateY(0);
- }
-
- .mt-sheet-handle {
- width: 36px;
- height: 4px;
- background: var(--border);
- border-radius: 2px;
- margin: 10px auto 6px;
- flex-shrink: 0;
- }
-
- .mt-sheet-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 16px 12px;
- flex-shrink: 0;
- }
- .mt-sheet-title {
- font-family: "JetBrains Mono", monospace;
- font-size: 0.8rem;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- }
- .mt-sheet-close {
- width: 28px; height: 28px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .mt-sheet-close:hover { color: var(--accent); border-color: var(--accent); }
- .mt-sheet-actions {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .mt-sheet-close-all {
- padding: 5px 10px;
- border: 1px solid var(--danger, #ef4444);
- border-radius: 6px;
- background: transparent;
- color: var(--danger, #ef4444);
- cursor: pointer;
- font-size: 0.7rem;
- font-weight: 600;
- transition: background 0.15s;
- }
- .mt-sheet-close-all:hover {
- background: rgba(239,68,68,0.1);
- }
-
- .mt-sheet-list {
- flex: 1;
- overflow-y: auto;
- padding: 0 12px 12px;
- -webkit-overflow-scrolling: touch;
- }
-
- .mt-tab-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border-radius: 8px;
- cursor: pointer;
- color: var(--text-primary);
- font-size: 0.85rem;
- transition: background 0.1s;
- margin-bottom: 2px;
- }
- .mt-tab-item:active {
- background: rgba(255,255,255,0.06);
- }
- .mt-tab-item.active {
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- color: var(--accent);
- font-weight: 600;
- }
- .mt-tab-item-icon {
- flex-shrink: 0;
- opacity: 0.6;
- }
- .mt-tab-item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .mt-tab-item-close {
- width: 24px; height: 24px;
- padding: 0;
- border: none;
- border-radius: 4px;
- background: transparent;
- color: var(--text-muted);
- cursor: pointer;
- font-size: 14px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .mt-tab-item-close:hover {
- color: var(--danger, #ef4444);
- background: rgba(239,68,68,0.1);
- }
-}
-
-/* ==========================================================================
- Backup / Diff Viewer Modal
- ========================================================================== */
-
-.backup-viewer-panel {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: min(90vw, 960px);
- height: min(85vh, 700px);
- background: var(--bg-primary, #1e1e2e);
- border: 1px solid var(--border);
- border-radius: 12px;
- box-shadow: 0 25px 60px rgba(0,0,0,0.5);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- z-index: 1001;
-}
-
-.backup-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 14px 18px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-secondary);
- flex-shrink: 0;
-}
-
-.backup-header-left {
- display: flex;
- align-items: baseline;
- gap: 10px;
- min-width: 0;
-}
-
-.backup-header h3 {
- margin: 0;
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.backup-file-path {
- font-size: 0.78rem;
- color: var(--text-muted);
- font-family: var(--font-mono, 'SF Mono', 'Fira Code', monospace);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.backup-header-right {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-shrink: 0;
-}
-
-.backup-count {
- font-size: 0.75rem;
- color: var(--text-muted);
- background: var(--bg-tertiary);
- padding: 3px 8px;
- border-radius: 4px;
-}
-
-.backup-close-btn {
- width: 36px;
- height: 36px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 150ms ease;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 auto;
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
-}
-
-.backup-close-btn:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-
-.backup-toolbar {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 18px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-primary);
- flex-shrink: 0;
-}
-
-.backup-selector-group {
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.backup-selector-group label {
- font-size: 0.78rem;
- color: var(--text-muted);
- white-space: nowrap;
-}
-
-.backup-select {
- padding: 6px 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-secondary);
- color: var(--text-primary);
- font-size: 0.8rem;
- font-family: var(--font-mono);
- max-width: 240px;
- cursor: pointer;
-}
-
-.backup-select:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.backup-vs-label {
- font-size: 0.75rem;
- color: var(--text-muted);
- font-weight: 600;
- user-select: none;
-}
-
-.backup-restore-btn {
- margin-left: auto;
- padding: 6px 14px;
- border: 1px solid var(--accent);
- border-radius: 6px;
- background: color-mix(in srgb, var(--accent) 10%, transparent);
- color: var(--accent);
- font-size: 0.8rem;
- font-weight: 500;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: background 0.15s, opacity 0.15s;
- white-space: nowrap;
-}
-
-.backup-restore-btn:hover {
- background: color-mix(in srgb, var(--accent) 20%, transparent);
-}
-
-.backup-restore-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.backup-restore-btn .icon-sm {
- width: 14px;
- height: 14px;
-}
-
-.backup-diff-container {
- flex: 1;
- overflow-y: auto;
- background: var(--bg-primary);
-}
-
-.backup-diff-loading,
-.backup-diff-error,
-.backup-diff-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: var(--text-muted);
- font-size: 0.85rem;
-}
-
-.backup-diff-error {
- color: var(--danger, #ef4444);
-}
-
-.backup-diff-output {
- padding: 0;
-}
-
-.backup-diff-table {
- width: 100%;
- border-collapse: collapse;
- font-family: var(--font-mono, 'SF Mono', 'Fira Code', 'Cascadia Code', monospace);
- font-size: 0.8rem;
- line-height: 1.55;
- table-layout: fixed;
-}
-
-.backup-diff-table td {
- padding: 0;
-}
-
-.backup-diff-table td.diff-prefix {
- width: 22px;
- text-align: center;
- user-select: none;
- font-weight: 600;
- padding: 1px 0;
- vertical-align: top;
-}
-
-.backup-diff-table td.diff-content {
- padding: 1px 8px;
- vertical-align: top;
-}
-
-.backup-diff-table td.diff-content pre {
- margin: 0;
- white-space: pre-wrap;
- word-break: break-all;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-
-.backup-diff-table tr.diff-add td {
- background: rgba(34, 197, 94, 0.12);
-}
-
-.backup-diff-table tr.diff-add td.diff-prefix {
- color: #22c55e;
-}
-
-.backup-diff-table tr.diff-del td {
- background: rgba(239, 68, 68, 0.12);
-}
-
-.backup-diff-table tr.diff-del td.diff-prefix {
- color: #ef4444;
-}
-
-.backup-diff-table tr.diff-hunk td {
- background: color-mix(in srgb, var(--accent) 8%, transparent);
- color: var(--accent);
- font-weight: 600;
-}
-
-.backup-diff-table tr.diff-header-line td {
- background: var(--bg-secondary);
- color: var(--text-muted);
- font-weight: 600;
-}
-
-.backup-diff-table tr.diff-context td {
- color: var(--text-secondary);
-}
-
-.backup-body {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: auto;
-}
-
-.backup-footer {
- padding: 6px 18px;
- border-top: 1px solid var(--border);
- background: var(--bg-secondary);
- flex-shrink: 0;
-}
-
-.backup-footer-hint {
- font-size: 0.7rem;
- color: var(--text-muted);
-}
-
-/* ── Side-by-side diff view ── */
-.backup-diff-sidebyside .sidebyside {
- table-layout: fixed;
-}
-
-.backup-diff-sidebyside td.sidebyside-left {
- border-right: 1px solid var(--border);
-}
-
-.backup-diff-sidebyside td.sidebyside-right {
- border-left: 1px solid var(--border);
-}
-
-.backup-diff-sidebyside td.diff-empty-cell {
- background: var(--bg-tertiary);
-}
-
-.backup-diff-mode-btn {
- padding: 6px 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 0.8rem;
- transition: all 150ms ease;
-}
-
-.backup-diff-mode-btn:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-
-.backup-diff-mode-btn .icon-sm {
- width: 14px;
- height: 14px;
-}
-
-/* ── Backup Manager ── */
-.backup-manager {
- padding: 16px 24px;
- max-width: 1000px;
- margin: 0 auto;
-}
-
-.backup-manager-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- gap: 12px;
-}
-
-.backup-manager-header h2 {
- margin: 0;
- font-size: 1.1rem;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.backup-manager-actions {
- display: flex;
- gap: 6px;
-}
-
-.backup-manager-filter {
- padding: 6px 10px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--bg-primary);
- color: var(--text-primary);
- font-size: 0.8rem;
- min-width: 200px;
-}
-
-.backup-manager-btn {
- width: 32px; height: 32px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-.backup-manager-btn:hover { color: var(--accent); border-color: var(--accent); }
-
-.backup-manager-stats { display: flex; gap: 16px; margin-bottom: 14px; }
-.bm-stat { font-size: 0.8rem; color: var(--text-muted); }
-
-.bm-vault-group { margin-bottom: 16px; }
-.bm-vault-name {
- font-size: 0.85rem; font-weight: 600; margin: 0 0 6px 0;
- display: flex; align-items: center; gap: 6px;
-}
-
-.bm-file-group {
- border: 1px solid var(--border); border-radius: 6px;
- margin-bottom: 8px; overflow: hidden;
-}
-
-.bm-file-header {
- display: flex; align-items: center; gap: 12px;
- padding: 8px 12px; background: var(--bg-secondary);
- border-bottom: 1px solid var(--border);
-}
-
-.bm-file-name { font-size: 0.8rem; font-weight: 500; display: flex; align-items: center; gap: 4px; }
-.bm-file-meta { font-size: 0.7rem; color: var(--text-muted); flex: 1; }
-
-.bm-file-purge {
- width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 4px;
- background: transparent; color: var(--text-muted); cursor: pointer;
- display: flex; align-items: center; justify-content: center;
-}
-.bm-file-purge:hover { color: var(--danger); border-color: var(--danger); }
-
-.bm-backup-list { padding: 2px 0; }
-
-.bm-backup-item {
- display: flex; align-items: center; gap: 12px;
- padding: 4px 12px; font-size: 0.75rem;
-}
-.bm-backup-item:hover { background: var(--bg-secondary); }
-
-.bm-backup-date { color: var(--text-secondary); min-width: 130px; font-family: var(--font-mono); }
-.bm-backup-size { color: var(--text-muted); flex: 1; }
-
-.bm-backup-delete {
- width: 22px; height: 22px; border: none; border-radius: 4px;
- background: transparent; color: var(--text-muted); cursor: pointer;
- display: flex; align-items: center; justify-content: center;
- opacity: 0;
-}
-.bm-backup-item:hover .bm-backup-delete { opacity: 1; }
-.bm-backup-delete:hover { color: var(--danger); background: rgba(239,68,68,0.1); }
-
-.backup-footer-manage-btn {
- background: none; border: 1px solid var(--border);
- border-radius: 4px; color: var(--accent);
- font-size: 0.7rem; cursor: pointer; padding: 3px 10px;
- transition: all 150ms ease;
-}
-.backup-footer-manage-btn:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
-
-.backup-manager-empty,
-.backup-manager-error {
- text-align: center; padding: 2rem; color: var(--text-muted); font-size: 0.85rem;
-}
-.backup-manager-error { color: var(--danger); }
-
-/* ==========================================================================
- Mermaid.js Diagrams
- ========================================================================== */
-
-.mermaid-container {
- margin: 12px 0;
- border: 1px solid var(--border);
- border-radius: 8px;
- overflow: hidden;
- background: var(--bg-secondary);
-}
-
-/* ── Header bar ── */
-.mermaid-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 6px 10px;
- background: var(--bg-hover);
- border-bottom: 1px solid var(--border);
- min-height: 34px;
-}
-.mermaid-diagram-type {
- font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
- font-size: 0.75rem; font-weight: 600;
- color: var(--text-secondary);
-}
-.mermaid-header-actions {
- display: flex; align-items: center; gap: 2px;
-}
-
-/* ── Buttons ── */
-.mermaid-btn {
- width: 28px; height: 28px;
- border: 1px solid transparent; border-radius: 4px;
- background: transparent; color: var(--text-secondary);
- cursor: pointer; display: flex; align-items: center; justify-content: center;
- transition: background 0.12s, color 0.12s;
-}
-.mermaid-btn:hover { background: var(--bg-primary); color: var(--accent); border-color: var(--border); }
-
-/* ── Code / Preview toggle ── */
-.mermaid-toggle {
- display: flex; margin-left: 8px;
- border: 1px solid var(--border); border-radius: 5px;
- overflow: hidden;
-}
-.mermaid-toggle-btn {
- padding: 2px 10px; font-size: 0.7rem; font-weight: 500;
- border: none; background: transparent;
- color: var(--text-muted, #999); cursor: pointer;
- transition: background 0.12s, color 0.12s;
-}
-.mermaid-toggle-btn.active {
- background: var(--bg-primary); color: var(--text-primary);
- box-shadow: 0 0 0 1px var(--border);
-}
-.mermaid-toggle-btn:first-child { border-right: 1px solid var(--border); }
-
-/* ── Body ── */
-.mermaid-body {
- position: relative;
-}
-.mermaid-rendered {
- padding: 16px;
- overflow: hidden;
- min-height: 80px;
- display: flex;
- justify-content: center;
-}
-.mermaid-rendered svg { max-width: 100%; height: auto; }
-
-.mermaid-source { padding: 16px; overflow: auto; }
-.mermaid-source pre {
- margin: 0; padding: 12px;
- background: var(--code-bg);
- border-radius: 4px;
- font-size: 0.78rem; line-height: 1.5;
- color: var(--text-primary);
-}
-
-/* ── Error ── */
-.mermaid-error {
- margin: 12px 0; padding: 12px 16px;
- border: 1px solid rgba(239,68,68,0.3); border-radius: 6px;
- background: rgba(239,68,68,0.08); color: var(--danger);
- font-size: 0.8rem; line-height: 1.5;
-}
-.mermaid-error strong { display: block; margin-bottom: 4px; }
-
-/* ── Fullscreen ── */
-.mermaid-fullscreen-overlay {
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
- z-index: 10000;
- display: flex; flex-direction: column;
-}
-/* Light theme — clean white like the example */
-[data-theme="light"] .mermaid-fullscreen-overlay {
- background: rgba(255,255,255,0.97);
-}
-[data-theme="light"] .mermaid-fullscreen-header {
- background: rgba(0,0,0,0.03);
- border-bottom: 1px solid rgba(0,0,0,0.08);
-}
-[data-theme="light"] .mermaid-fullscreen-header .mermaid-diagram-type {
- color: rgba(0,0,0,0.5);
-}
-[data-theme="light"] .mermaid-fullscreen-header .mermaid-btn {
- color: rgba(0,0,0,0.4);
-}
-[data-theme="light"] .mermaid-fullscreen-header .mermaid-btn:hover {
- background: rgba(0,0,0,0.05);
- color: rgba(0,0,0,0.8);
-}
-
-/* Dark theme */
-[data-theme="dark"] .mermaid-fullscreen-overlay {
- background: rgba(18,18,24,0.97);
-}
-[data-theme="dark"] .mermaid-fullscreen-header {
- background: rgba(255,255,255,0.06);
- border-bottom: 1px solid rgba(255,255,255,0.08);
-}
-[data-theme="dark"] .mermaid-fullscreen-header .mermaid-diagram-type {
- color: rgba(255,255,255,0.7);
-}
-[data-theme="dark"] .mermaid-fullscreen-header .mermaid-btn {
- color: rgba(255,255,255,0.6);
-}
-[data-theme="dark"] .mermaid-fullscreen-header .mermaid-btn:hover {
- background: rgba(255,255,255,0.1);
- color: #fff;
-}
-
-.mermaid-fullscreen-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 10px 16px;
- min-height: 46px;
- flex-shrink: 0;
-}
-.mermaid-fullscreen-content {
- flex: 1;
- display: flex; align-items: center; justify-content: center;
- padding: 16px;
- overflow: hidden;
-}
-.mermaid-fullscreen-content svg {
- width: 100%; height: 100%;
- max-width: 100%; max-height: 100%;
-}
-
-/* ── Dark mode overrides for beta diagrams that ignore themeVariables ── */
-
-/* packet-beta */
-[data-theme="dark"] .mermaid-rendered svg .row rect,
-[data-theme="dark"] .mermaid-rendered svg g[id*="packet"] rect {
- fill: #1e3a5f !important;
- stroke: #4a7ab5 !important;
-}
-[data-theme="dark"] .mermaid-rendered svg .row text,
-[data-theme="dark"] .mermaid-rendered svg g[id*="packet"] text {
- fill: #c8daf0 !important;
-}
-[data-theme="dark"] .mermaid-fullscreen-content svg .row rect,
-[data-theme="dark"] .mermaid-fullscreen-content svg g[id*="packet"] rect {
- fill: #1e3a5f !important;
- stroke: #4a7ab5 !important;
-}
-[data-theme="dark"] .mermaid-fullscreen-content svg .row text,
-[data-theme="dark"] .mermaid-fullscreen-content svg g[id*="packet"] text {
- fill: #c8daf0 !important;
-}
-
-/* xychart-beta — bar fills and line color */
-[data-theme="dark"] .mermaid-rendered svg rect[class*="plot"],
-[data-theme="dark"] .mermaid-rendered svg rect[id*="bar"] {
- fill: #1e3a5f !important;
- stroke: #4a7ab5 !important;
-}
-[data-theme="dark"] .mermaid-rendered svg path[class*="plot"],
-[data-theme="dark"] .mermaid-rendered svg path[id*="line"] {
- stroke: #6a9ad0 !important;
-}
-[data-theme="dark"] .mermaid-fullscreen-content svg rect[class*="plot"],
-[data-theme="dark"] .mermaid-fullscreen-content svg rect[id*="bar"] {
- fill: #1e3a5f !important;
- stroke: #4a7ab5 !important;
-}
-[data-theme="dark"] .mermaid-fullscreen-content svg path[class*="plot"],
-[data-theme="dark"] .mermaid-fullscreen-content svg path[id*="line"] {
- stroke: #6a9ad0 !important;
-}
-
-.mermaid-focus-panel {
- position: fixed; top: 0; right: 0; bottom: 0; width: 480px;
- z-index: 9998; background: var(--bg-secondary);
- border-left: 1px solid var(--border);
- box-shadow: -4px 0 24px rgba(0,0,0,0.3);
- display: flex; flex-direction: column;
-}
-.mermaid-focus-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 10px 14px; border-bottom: 1px solid var(--border);
- font-size: 0.85rem; font-weight: 600;
-}
-.mermaid-focus-close {
- width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 4px;
- background: transparent; color: var(--text-muted); cursor: pointer;
- display: flex; align-items: center; justify-content: center;
-}
-.mermaid-focus-close:hover { color: var(--accent); }
-.mermaid-focus-content { flex: 1; overflow: auto; padding: 16px; }
-
-/* Mermaid editor live preview panel */
-.mermaid-live-preview {
- border-top: 1px solid var(--border);
- background: var(--bg-secondary);
- min-height: 100px; max-height: 300px;
- overflow: auto; padding: 12px;
-}
-.mermaid-live-preview .mermaid-error { margin: 0; }
-
-/* Manager body: list + preview side panel */
-.backup-manager-body { display: flex; gap: 0; min-height: 400px; }
-
-.backup-manager-list { flex: 1; min-width: 0; }
-
-.backup-manager-preview {
- width: 400px; flex-shrink: 0;
- border-left: 1px solid var(--border);
- background: var(--bg-primary);
- display: flex; flex-direction: column;
-}
-.bm-preview-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 10px 14px; border-bottom: 1px solid var(--border);
-}
-.bm-preview-header h3 { margin: 0; font-size: 0.85rem; }
-.bm-preview-content {
- flex: 1; overflow: auto; padding: 12px 14px;
- font-family: var(--font-mono); font-size: 0.75rem; line-height: 1.55;
- white-space: pre-wrap; word-break: break-all;
- color: var(--text-secondary); margin: 0;
-}
-
-/* Vault header with purge button */
-.bm-vault-header {
- display: flex; align-items: center; gap: 12px;
- padding: 8px 0; border-bottom: 1px solid var(--border); margin-bottom: 8px;
-}
-.bm-vault-meta { font-size: 0.7rem; color: var(--text-muted); flex: 1; }
-
-.bm-vault-purge {
- padding: 3px 10px; border: 1px solid var(--border); border-radius: 4px;
- background: transparent; color: var(--danger); cursor: pointer;
- font-size: 0.7rem; display: flex; align-items: center; gap: 4px;
- transition: all 150ms ease;
-}
-.bm-vault-purge:hover { background: rgba(239,68,68,0.08); border-color: var(--danger); }
-
-/* Backup item actions */
-.bm-backup-actions { display: flex; gap: 2px; opacity: 0; transition: opacity 0.12s; }
-.bm-backup-item:hover .bm-backup-actions { opacity: 1; }
-
-.bm-backup-preview,
-.bm-backup-restore {
- width: 22px; height: 22px; border: none; border-radius: 4px;
- background: transparent; color: var(--text-muted); cursor: pointer;
- display: flex; align-items: center; justify-content: center;
-}
-.bm-backup-preview:hover { color: var(--accent); background: rgba(59,130,246,0.1); }
-.bm-backup-restore:hover { color: #22c55e; background: rgba(34,197,94,0.1); }
-
-.bm-backup-item.active {
- background: color-mix(in srgb, var(--accent) 10%, transparent);
- border-radius: 4px;
-}
-
-/* ── About Modal ── */
-.about-overlay {
- position: fixed; inset: 0; z-index: 1000;
- background: rgba(0,0,0,0.7); backdrop-filter: blur(6px);
- display: none; align-items: center; justify-content: center;
-}
-.about-overlay.active { display: flex; }
-.about-modal {
- background: var(--bg-secondary);
- border: 1px solid var(--border-md);
- border-radius: 16px;
- width: 540px; max-width: 94vw; max-height: 90vh;
- overflow-y: auto;
- position: relative;
- box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
- animation: about-in 0.35s ease;
-}
-@keyframes about-in {
- from { opacity: 0; transform: translateY(20px) scale(0.97); }
- to { opacity: 1; transform: translateY(0) scale(1); }
-}
-.about-close {
- position: absolute; top: 12px; right: 14px;
- background: var(--bg-hover); border: none; color: var(--text-secondary);
- width: 30px; height: 30px; border-radius: 8px; font-size: 18px;
- cursor: pointer; display: flex; align-items: center; justify-content: center;
- transition: background 0.15s, color 0.15s; z-index: 1;
-}
-.about-close:hover { background: var(--danger-bg); color: var(--danger); }
-.about-hero {
- text-align: center; padding: 22px 24px 16px;
- background: linear-gradient(160deg, var(--accent-bg) 0%, transparent 60%);
- border-bottom: 1px solid var(--border);
-}
-.about-logo { margin-bottom: 8px; display: inline-block; }
-.about-title {
- font-size: 1.4rem; font-weight: 700; color: var(--text-primary);
- letter-spacing: -0.5px; margin-bottom: 4px;
-}
-.about-tagline {
- font-size: 0.8rem; color: var(--text-secondary); line-height: 1.4;
- margin-bottom: 8px;
-}
-.about-version {
- display: inline-block; padding: 3px 12px;
- background: var(--accent); color: #fff;
- font-size: 0.7rem; font-weight: 600; font-family: var(--mono);
- border-radius: 20px; letter-spacing: 0.5px;
-}
-.about-body { padding: 12px 28px 16px; }
-.about-stats {
- display: flex; gap: 0; justify-content: center;
- margin-bottom: 10px;
-}
-.about-stat {
- flex: 1; text-align: center; padding: 8px 8px;
- border-right: 1px solid var(--border);
-}
-.about-stat:last-child { border-right: none; }
-.about-stat-value {
- display: block; font-size: 1.3rem; font-weight: 700;
- color: var(--accent); font-family: var(--mono);
-}
-.about-stat-label {
- font-size: 0.6rem; color: var(--text-muted);
- text-transform: uppercase; letter-spacing: 0.6px; margin-top: 1px;
-}
-.about-stack { margin-bottom: 10px; }
-.about-stack h3 {
- font-size: 0.65rem; color: var(--text-muted); text-transform: uppercase;
- letter-spacing: 0.8px; margin-bottom: 6px;
-}
-.about-stack-grid {
- display: flex; flex-wrap: wrap; gap: 5px;
-}
-.about-stack-badge {
- padding: 2px 8px; font-size: 0.65rem;
- background: var(--bg-hover); color: var(--text-secondary);
- border: 1px solid var(--border); border-radius: 6px;
- font-family: var(--mono);
-}
-.about-footer { border-top: 1px solid var(--border); padding-top: 10px; }
-.about-meta {
- display: flex; gap: 16px; font-size: 0.65rem; color: var(--text-muted);
- margin-bottom: 6px; flex-wrap: wrap;
-}
-.about-meta strong { color: var(--text-secondary); font-family: var(--mono); font-size: 0.65rem; }
-.about-joke {
- font-size: 0.68rem; color: var(--text-muted); font-style: italic;
- margin-bottom: 6px; opacity: 0.7;
-}
-.about-copy {
- font-size: 0.62rem; color: var(--text-muted); opacity: 0.5;
-}
-.about-copy code { font-family: var(--mono); font-size: 0.58rem; }
-
-/* ── PDF Viewer ── */
-.pdf-viewer-container {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-.pdf-toolbar {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 16px;
- background: var(--surface1);
- border-bottom: 1px solid var(--border);
- font-size: 0.85rem;
- flex-shrink: 0;
-}
-.pdf-info {
- flex: 1;
- color: var(--text-dim);
-}
-.pdf-iframe {
- flex: 1;
- width: 100%;
- border: none;
- min-height: 70vh;
-}
-
-/* ── PaneManager — Split View ── */
-.pane-grid {
- display: grid;
- height: 100%;
- overflow: hidden;
-}
-.pane-container {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- position: relative;
-}
-.pane-container.pane-active {
- outline: 2px solid var(--accent);
- outline-offset: -2px;
- z-index: 1;
-}
-.pane-tab-bar { flex-shrink: 0; }
-.pane-content { flex: 1; overflow-y: auto; }
-.pane-resize-handle {
- background: var(--border);
- transition: background 150ms;
- min-width: 4px;
- min-height: 4px;
- z-index: 2;
-}
-.pane-resize-handle:hover {
- background: var(--accent);
-}
-.code-block-wrapper:hover .copy-code-btn {
- opacity: 1;
-}
-.copy-code-btn {
- position: absolute;
- top: 8px;
- right: 8px;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 4px 10px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- color: var(--text-secondary);
- font-size: 11px;
- font-family: var(--mono);
- cursor: pointer;
- opacity: 0;
- transition: opacity 150ms ease, background 150ms ease, color 150ms ease;
- z-index: 5;
-}
-.copy-code-btn:hover {
- background: var(--accent);
- color: #fff;
- border-color: var(--accent);
-}
-.copy-code-btn.copied {
- background: var(--accent-green);
- color: #fff;
- border-color: var(--accent-green);
-}
-
-/* Mobile : toujours visible (pas de hover sur tactile) */
-@media (max-width: 768px) {
- .copy-code-btn {
- opacity: 1;
- padding: 2px 8px;
- font-size: 10px;
- top: 4px;
- right: 4px;
- }
-}
diff --git a/desktop/frontend-static/sw.js b/desktop/frontend-static/sw.js
deleted file mode 100644
index 073b93d..0000000
--- a/desktop/frontend-static/sw.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */
-
-const CACHE_VERSION = 'obsigate-v6-i18n';
-const STATIC_CACHE = CACHE_VERSION + '-static';
-const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic';
-const MAX_DYNAMIC_CACHE_SIZE = 100;
-
-const STATIC_ASSETS = [
- '/',
- '/static/index.html',
- '/static/app.js',
- '/static/style.css',
- '/static/manifest.json',
- '/static/js/viewer.js',
- '/static/js/i18n.js',
- '/static/js/auth.js',
- '/static/js/dashboard.js',
- '/static/js/app.js',
- '/static/locales/en.json',
- '/static/locales/fr.json'
-];
-
-/* ── Install ── */
-self.addEventListener('install', (event) => {
- console.log('[SW] Installing v' + CACHE_VERSION);
- event.waitUntil(
- caches.open(STATIC_CACHE)
- .then((cache) => {
- console.log('[SW] Caching static assets');
- return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
- })
- .catch((err) => console.error('[SW] Cache error:', err))
- .then(() => self.skipWaiting())
- );
-});
-
-/* ── Activate ── */
-self.addEventListener('activate', (event) => {
- console.log('[SW] Activating...');
- event.waitUntil(
- caches.keys()
- .then((cacheNames) => {
- return Promise.all(
- cacheNames
- .filter((name) => name.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
- .map((name) => {
- console.log('[SW] Deleting old cache:', name);
- return caches.delete(name);
- })
- );
- })
- .then(() => self.clients.claim())
- );
-});
-
-/* ── Fetch ── */
-self.addEventListener('fetch', (event) => {
- const { request } = event;
- const url = new URL(request.url);
-
- if (request.method !== 'GET') return;
- if (url.pathname === '/api/events') return;
- if (url.pathname.startsWith('/api/auth/')) return;
-
- // API requests: Network first, tell client to use IndexedDB if offline
- if (url.pathname.startsWith('/api/')) {
- event.respondWith(networkFirstAPI(request));
- return;
- }
-
- // Static assets: Cache first
- event.respondWith(cacheFirstStatic(request, url));
-});
-
-/* ── Network First for API ── */
-async function networkFirstAPI(request) {
- try {
- const response = await fetch(request);
-
- // Cache successful GETs in dynamic cache
- if (response && response.status === 200) {
- const cache = await caches.open(DYNAMIC_CACHE);
- cache.put(request, response.clone());
- limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
- }
- return response;
- } catch (error) {
- console.log('[SW] Offline — checking cache for:', request.url);
-
- // Try dynamic cache first
- const cached = await caches.match(request);
- if (cached) {
- // Add offline header so the page can detect
- const headers = new Headers(cached.headers);
- headers.set('X-ObsiGate-Offline', 'true');
- return new Response(cached.body, { status: cached.status, statusText: cached.statusText, headers });
- }
-
- // Fallback: tell the page to use IndexedDB
- return new Response(JSON.stringify({
- offline: true,
- endpoint: new URL(request.url).pathname,
- message: 'Vous etes hors-ligne. Les donnees locales sont utilisees.'
- }), {
- status: 200,
- headers: { 'Content-Type': 'application/json', 'X-ObsiGate-Offline': 'true' }
- });
- }
-}
-
-/* ── Cache First for Static ── */
-async function cacheFirstStatic(request, url) {
- const cached = await caches.match(request);
- if (cached) return cached;
-
- try {
- const response = await fetch(request);
- if (response && response.status === 200) {
- const cache = await caches.open(STATIC_CACHE);
- cache.put(request, response.clone());
- }
- return response;
- } catch {
- // Offline fallback for HTML navigation
- if (request.mode === 'navigate' || url.pathname === '/' || url.pathname.endsWith('/')) {
- const offlinePage = await caches.match('/static/index.html');
- if (offlinePage) return offlinePage;
- }
- return new Response('Offline', { status: 503 });
- }
-}
-
-async function limitCacheSize(cacheName, maxSize) {
- const cache = await caches.open(cacheName);
- const keys = await cache.keys();
- if (keys.length > maxSize) {
- for (let i = 0; i < keys.length - maxSize; i++) {
- await cache.delete(keys[i]);
- }
- }
-}
-
-/* ── Messages ── */
-self.addEventListener('message', (event) => {
- const data = event.data;
- if (!data || !data.type) return;
-
- switch (data.type) {
- case 'SKIP_WAITING':
- self.skipWaiting();
- break;
- case 'CLEAR_CACHE':
- event.waitUntil(
- caches.keys().then(names => Promise.all(names.map(n => caches.delete(n))))
- );
- break;
- case 'CLAIM_CLIENTS':
- event.waitUntil(self.clients.claim());
- break;
- }
-});
-
-/* ── Sync ── */
-self.addEventListener('sync', (event) => {
- console.log('[SW] Background sync:', event.tag);
- if (event.tag === 'obsigate-sync') {
- event.waitUntil(
- self.clients.matchAll().then(clients => {
- clients.forEach(client => {
- client.postMessage({ type: 'SYNC_TRIGGERED' });
- });
- })
- );
- }
-});
-
-/* ── Push ── */
-self.addEventListener('push', (event) => {
- const options = {
- body: event.data ? event.data.text() : 'Nouvelle mise a jour',
- icon: '/static/icons/icon-192x192.svg',
- badge: '/static/icons/icon-72x72.svg',
- vibrate: [200, 100, 200],
- actions: [
- { action: 'open', title: 'Ouvrir ObsiGate' },
- { action: 'close', title: 'Fermer' }
- ]
- };
- event.waitUntil(self.registration.showNotification('ObsiGate', options));
-});
-
-self.addEventListener('notificationclick', (event) => {
- event.notification.close();
- if (event.action === 'open') {
- event.waitUntil(clients.openWindow('/'));
- }
-});
diff --git a/desktop/python-embed/LICENSE.txt b/desktop/python-embed/LICENSE.txt
deleted file mode 100644
index 7a0193a..0000000
--- a/desktop/python-embed/LICENSE.txt
+++ /dev/null
@@ -1,702 +0,0 @@
-A. HISTORY OF THE SOFTWARE
-==========================
-
-Python was created in the early 1990s by Guido van Rossum at Stichting
-Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
-as a successor of a language called ABC. Guido remains Python's
-principal author, although it includes many contributions from others.
-
-In 1995, Guido continued his work on Python at the Corporation for
-National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
-in Reston, Virginia where he released several versions of the
-software.
-
-In May 2000, Guido and the Python core development team moved to
-BeOpen.com to form the BeOpen PythonLabs team. In October of the same
-year, the PythonLabs team moved to Digital Creations, which became
-Zope Corporation. In 2001, the Python Software Foundation (PSF, see
-https://www.python.org/psf/) was formed, a non-profit organization
-created specifically to own Python-related Intellectual Property.
-Zope Corporation was a sponsoring member of the PSF.
-
-All Python releases are Open Source (see https://opensource.org for
-the Open Source Definition). Historically, most, but not all, Python
-releases have also been GPL-compatible; the table below summarizes
-the various releases.
-
- Release Derived Year Owner GPL-
- from compatible? (1)
-
- 0.9.0 thru 1.2 1991-1995 CWI yes
- 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
- 1.6 1.5.2 2000 CNRI no
- 2.0 1.6 2000 BeOpen.com no
- 1.6.1 1.6 2001 CNRI yes (2)
- 2.1 2.0+1.6.1 2001 PSF no
- 2.0.1 2.0+1.6.1 2001 PSF yes
- 2.1.1 2.1+2.0.1 2001 PSF yes
- 2.1.2 2.1.1 2002 PSF yes
- 2.1.3 2.1.2 2002 PSF yes
- 2.2 and above 2.1.1 2001-now PSF yes
-
-Footnotes:
-
-(1) GPL-compatible doesn't mean that we're distributing Python under
- the GPL. All Python licenses, unlike the GPL, let you distribute
- a modified version without making your changes open source. The
- GPL-compatible licenses make it possible to combine Python with
- other software that is released under the GPL; the others don't.
-
-(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
- because its license has a choice of law clause. According to
- CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
- is "not incompatible" with the GPL.
-
-Thanks to the many outside volunteers who have worked under Guido's
-direction to make these releases possible.
-
-
-B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
-===============================================================
-
-Python software and documentation are licensed under the
-Python Software Foundation License Version 2.
-
-Starting with Python 3.8.6, examples, recipes, and other code in
-the documentation are dual licensed under the PSF License Version 2
-and the Zero-Clause BSD license.
-
-Some software incorporated into Python is under different licenses.
-The licenses are listed with code falling under that license.
-
-
-PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
---------------------------------------------
-
-1. This LICENSE AGREEMENT is between the Python Software Foundation
-("PSF"), and the Individual or Organization ("Licensee") accessing and
-otherwise using this software ("Python") in source or binary form and
-its associated documentation.
-
-2. Subject to the terms and conditions of this License Agreement, PSF hereby
-grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
-analyze, test, perform and/or display publicly, prepare derivative works,
-distribute, and otherwise use Python alone or in any derivative version,
-provided, however, that PSF's License Agreement and PSF's notice of copyright,
-i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
-2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
-All Rights Reserved" are retained in Python alone or in any derivative version
-prepared by Licensee.
-
-3. In the event Licensee prepares a derivative work that is based on
-or incorporates Python or any part thereof, and wants to make
-the derivative work available to others as provided herein, then
-Licensee hereby agrees to include in any such work a brief summary of
-the changes made to Python.
-
-4. PSF is making Python available to Licensee on an "AS IS"
-basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
-INFRINGE ANY THIRD PARTY RIGHTS.
-
-5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
-FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
-A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
-OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
-
-6. This License Agreement will automatically terminate upon a material
-breach of its terms and conditions.
-
-7. Nothing in this License Agreement shall be deemed to create any
-relationship of agency, partnership, or joint venture between PSF and
-Licensee. This License Agreement does not grant permission to use PSF
-trademarks or trade name in a trademark sense to endorse or promote
-products or services of Licensee, or any third party.
-
-8. By copying, installing or otherwise using Python, Licensee
-agrees to be bound by the terms and conditions of this License
-Agreement.
-
-
-BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
--------------------------------------------
-
-BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
-
-1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
-office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
-Individual or Organization ("Licensee") accessing and otherwise using
-this software in source or binary form and its associated
-documentation ("the Software").
-
-2. Subject to the terms and conditions of this BeOpen Python License
-Agreement, BeOpen hereby grants Licensee a non-exclusive,
-royalty-free, world-wide license to reproduce, analyze, test, perform
-and/or display publicly, prepare derivative works, distribute, and
-otherwise use the Software alone or in any derivative version,
-provided, however, that the BeOpen Python License is retained in the
-Software, alone or in any derivative version prepared by Licensee.
-
-3. BeOpen is making the Software available to Licensee on an "AS IS"
-basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
-INFRINGE ANY THIRD PARTY RIGHTS.
-
-4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
-SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
-AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
-DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
-
-5. This License Agreement will automatically terminate upon a material
-breach of its terms and conditions.
-
-6. This License Agreement shall be governed by and interpreted in all
-respects by the law of the State of California, excluding conflict of
-law provisions. Nothing in this License Agreement shall be deemed to
-create any relationship of agency, partnership, or joint venture
-between BeOpen and Licensee. This License Agreement does not grant
-permission to use BeOpen trademarks or trade names in a trademark
-sense to endorse or promote products or services of Licensee, or any
-third party. As an exception, the "BeOpen Python" logos available at
-http://www.pythonlabs.com/logos.html may be used according to the
-permissions granted on that web page.
-
-7. By copying, installing or otherwise using the software, Licensee
-agrees to be bound by the terms and conditions of this License
-Agreement.
-
-
-CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
----------------------------------------
-
-1. This LICENSE AGREEMENT is between the Corporation for National
-Research Initiatives, having an office at 1895 Preston White Drive,
-Reston, VA 20191 ("CNRI"), and the Individual or Organization
-("Licensee") accessing and otherwise using Python 1.6.1 software in
-source or binary form and its associated documentation.
-
-2. Subject to the terms and conditions of this License Agreement, CNRI
-hereby grants Licensee a nonexclusive, royalty-free, world-wide
-license to reproduce, analyze, test, perform and/or display publicly,
-prepare derivative works, distribute, and otherwise use Python 1.6.1
-alone or in any derivative version, provided, however, that CNRI's
-License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
-1995-2001 Corporation for National Research Initiatives; All Rights
-Reserved" are retained in Python 1.6.1 alone or in any derivative
-version prepared by Licensee. Alternately, in lieu of CNRI's License
-Agreement, Licensee may substitute the following text (omitting the
-quotes): "Python 1.6.1 is made available subject to the terms and
-conditions in CNRI's License Agreement. This Agreement together with
-Python 1.6.1 may be located on the internet using the following
-unique, persistent identifier (known as a handle): 1895.22/1013. This
-Agreement may also be obtained from a proxy server on the internet
-using the following URL: http://hdl.handle.net/1895.22/1013".
-
-3. In the event Licensee prepares a derivative work that is based on
-or incorporates Python 1.6.1 or any part thereof, and wants to make
-the derivative work available to others as provided herein, then
-Licensee hereby agrees to include in any such work a brief summary of
-the changes made to Python 1.6.1.
-
-4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
-basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
-IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
-DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
-FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
-INFRINGE ANY THIRD PARTY RIGHTS.
-
-5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
-1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
-A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
-OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
-
-6. This License Agreement will automatically terminate upon a material
-breach of its terms and conditions.
-
-7. This License Agreement shall be governed by the federal
-intellectual property law of the United States, including without
-limitation the federal copyright law, and, to the extent such
-U.S. federal law does not apply, by the law of the Commonwealth of
-Virginia, excluding Virginia's conflict of law provisions.
-Notwithstanding the foregoing, with regard to derivative works based
-on Python 1.6.1 that incorporate non-separable material that was
-previously distributed under the GNU General Public License (GPL), the
-law of the Commonwealth of Virginia shall govern this License
-Agreement only as to issues arising under or with respect to
-Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
-License Agreement shall be deemed to create any relationship of
-agency, partnership, or joint venture between CNRI and Licensee. This
-License Agreement does not grant permission to use CNRI trademarks or
-trade name in a trademark sense to endorse or promote products or
-services of Licensee, or any third party.
-
-8. By clicking on the "ACCEPT" button where indicated, or by copying,
-installing or otherwise using Python 1.6.1, Licensee agrees to be
-bound by the terms and conditions of this License Agreement.
-
- ACCEPT
-
-
-CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
---------------------------------------------------
-
-Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
-The Netherlands. All rights reserved.
-
-Permission to use, copy, modify, and distribute this software and its
-documentation for any purpose and without fee is hereby granted,
-provided that the above copyright notice appear in all copies and that
-both that copyright notice and this permission notice appear in
-supporting documentation, and that the name of Stichting Mathematisch
-Centrum or CWI not be used in advertising or publicity pertaining to
-distribution of the software without specific, written prior
-permission.
-
-STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
-THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
-FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
-OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
-----------------------------------------------------------------------
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-
-
-
-Additional Conditions for this Windows binary build
----------------------------------------------------
-
-This program is linked with and uses Microsoft Distributable Code,
-copyrighted by Microsoft Corporation. The Microsoft Distributable Code
-is embedded in each .exe, .dll and .pyd file as a result of running
-the code through a linker.
-
-If you further distribute programs that include the Microsoft
-Distributable Code, you must comply with the restrictions on
-distribution specified by Microsoft. In particular, you must require
-distributors and external end users to agree to terms that protect the
-Microsoft Distributable Code at least as much as Microsoft's own
-requirements for the Distributable Code. See Microsoft's documentation
-(included in its developer tools and on its website at microsoft.com)
-for specific details.
-
-Redistribution of the Windows binary build of the Python interpreter
-complies with this agreement, provided that you do not:
-
-- alter any copyright, trademark or patent notice in Microsoft's
-Distributable Code;
-
-- use Microsoft's trademarks in your programs' names or in a way that
-suggests your programs come from or are endorsed by Microsoft;
-
-- distribute Microsoft's Distributable Code to run on a platform other
-than Microsoft operating systems, run-time technologies or application
-platforms; or
-
-- include Microsoft Distributable Code in malicious, deceptive or
-unlawful programs.
-
-These restrictions apply only to the Microsoft Distributable Code as
-defined above, not to Python itself or any programs running on the
-Python interpreter. The redistribution of the Python interpreter and
-libraries is governed by the Python Software License included with this
-file, or by other licenses as marked.
-
-
-
---------------------------------------------------------------------------
-
-This program, "bzip2", the associated library "libbzip2", and all
-documentation, are copyright (C) 1996-2019 Julian R Seward. All
-rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
-2. The origin of this software must not be misrepresented; you must
- not claim that you wrote the original software. If you use this
- software in a product, an acknowledgment in the product
- documentation would be appreciated but is not required.
-
-3. Altered source versions must be plainly marked as such, and must
- not be misrepresented as being the original software.
-
-4. The name of the author may not be used to endorse or promote
- products derived from this software without specific prior written
- permission.
-
-THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
-OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
-GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-Julian Seward, jseward@acm.org
-bzip2/libbzip2 version 1.0.8 of 13 July 2019
-
---------------------------------------------------------------------------
-
-libffi - Copyright (c) 1996-2022 Anthony Green, Red Hat, Inc and others.
-See source files for details.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-``Software''), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-
- Apache License
- Version 2.0, January 2004
- https://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
-This software is copyrighted by the Regents of the University of
-California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
-Corporation and other parties. The following terms apply to all files
-associated with the software unless explicitly disclaimed in
-individual files.
-
-The authors hereby grant permission to use, copy, modify, distribute,
-and license this software and its documentation for any purpose, provided
-that existing copyright notices are retained in all copies and that this
-notice is included verbatim in any distributions. No written agreement,
-license, or royalty fee is required for any of the authorized uses.
-Modifications to this software may be copyrighted by their authors
-and need not follow the licensing terms described here, provided that
-the new terms are clearly indicated on the first page of each file where
-they apply.
-
-IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
-FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
-ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
-DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-
-THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
-IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
-NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
-MODIFICATIONS.
-
-GOVERNMENT USE: If you are acquiring this software on behalf of the
-U.S. government, the Government shall have only "Restricted Rights"
-in the software and related documentation as defined in the Federal
-Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
-are acquiring the software on behalf of the Department of Defense, the
-software shall be classified as "Commercial Computer Software" and the
-Government shall have only "Restricted Rights" as defined in Clause
-252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the
-authors grant the U.S. Government and others acting in its behalf
-permission to use and distribute the software in accordance with the
-terms specified in this license.
-
-This software is copyrighted by the Regents of the University of
-California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
-Corporation, Apple Inc. and other parties. The following terms apply to
-all files associated with the software unless explicitly disclaimed in
-individual files.
-
-The authors hereby grant permission to use, copy, modify, distribute,
-and license this software and its documentation for any purpose, provided
-that existing copyright notices are retained in all copies and that this
-notice is included verbatim in any distributions. No written agreement,
-license, or royalty fee is required for any of the authorized uses.
-Modifications to this software may be copyrighted by their authors
-and need not follow the licensing terms described here, provided that
-the new terms are clearly indicated on the first page of each file where
-they apply.
-
-IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
-FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
-ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
-DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-
-THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
-IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
-NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
-MODIFICATIONS.
-
-GOVERNMENT USE: If you are acquiring this software on behalf of the
-U.S. government, the Government shall have only "Restricted Rights"
-in the software and related documentation as defined in the Federal
-Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
-are acquiring the software on behalf of the Department of Defense, the
-software shall be classified as "Commercial Computer Software" and the
-Government shall have only "Restricted Rights" as defined in Clause
-252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the
-authors grant the U.S. Government and others acting in its behalf
-permission to use and distribute the software in accordance with the
-terms specified in this license.
-
-Copyright (c) 1993-1999 Ioi Kim Lam.
-Copyright (c) 2000-2001 Tix Project Group.
-Copyright (c) 2004 ActiveState
-
-This software is copyrighted by the above entities
-and other parties. The following terms apply to all files associated
-with the software unless explicitly disclaimed in individual files.
-
-The authors hereby grant permission to use, copy, modify, distribute,
-and license this software and its documentation for any purpose, provided
-that existing copyright notices are retained in all copies and that this
-notice is included verbatim in any distributions. No written agreement,
-license, or royalty fee is required for any of the authorized uses.
-Modifications to this software may be copyrighted by their authors
-and need not follow the licensing terms described here, provided that
-the new terms are clearly indicated on the first page of each file where
-they apply.
-
-IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
-FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
-ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
-DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-
-THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
-IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
-NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
-MODIFICATIONS.
-
-GOVERNMENT USE: If you are acquiring this software on behalf of the
-U.S. government, the Government shall have only "Restricted Rights"
-in the software and related documentation as defined in the Federal
-Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
-are acquiring the software on behalf of the Department of Defense, the
-software shall be classified as "Commercial Computer Software" and the
-Government shall have only "Restricted Rights" as defined in Clause
-252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
-authors grant the U.S. Government and others acting in its behalf
-permission to use and distribute the software in accordance with the
-terms specified in this license.
-
-----------------------------------------------------------------------
-
-Parts of this software are based on the Tcl/Tk software copyrighted by
-the Regents of the University of California, Sun Microsystems, Inc.,
-and other parties. The original license terms of the Tcl/Tk software
-distribution is included in the file docs/license.tcltk.
-
-Parts of this software are based on the HTML Library software
-copyrighted by Sun Microsystems, Inc. The original license terms of
-the HTML Library software distribution is included in the file
-docs/license.html_lib.
-
diff --git a/desktop/python-embed/_asyncio.pyd b/desktop/python-embed/_asyncio.pyd
deleted file mode 100644
index b85d1a1e0e6cf56051fc150b14a87eadc46fb30a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 65304
zcmeFa34D~*^*4S4nGgao5gCp9fI)+jFpz*jz)na4&&UKK1Wc8hOfnOal4QnNByo)<
zSmI+UZY{QI!y>C9wpQa(8w3RfHBzf_X{Gzb&^G#WLEAd-_uS=~X9iHa{QmFf|9Ly`
z%-zpD_ndRjId^#`WtXpE!x>}Ac%o6pdJxjb!N0%!D>I3)6V86>1oqg-m&f%axn3Su
zQQr`>`2)V?0e6$F*4^Cf3)yPCwm`Vq*3fJ#S-9BN}n9Vt&D{3XgM{2)ctd+LU}MqJGEf0Xhx5AT#=
z(+H7X!_$izYU`=5qv}+hAvWGNYS;!G|6=5ciw}A{I
zK8o)rrj5edZI@LUob#;TDJjZ|=STI4vkGgb~*YY=(}<`84uBM}~A>@Z>9$5_^6
zge-}zMZeW68LOb=aXz-I!BdW?tl&pTOh3*iFXU|rp=<{sk%@=Ko>1S0Zu0^jcgT%;
zm!O+zUA4wU5iMMXZwLnute^AsM_$3gG{!9XqY
zI8BBSNyGCT9@EFc>vTI0VO!)681bs!jMZ--
z)Vk6){RG;;x*B|?%v~b$gGVVdC^H`tnUkKN%qwN)_mTP3$IvH$G3=*QQh*Xo@7GrK
zhOXDTtm!)`xi4zHty;i7Wh_;;mZGwz?~htTGCz6}L`k=VU*^ScfP*M@Mgj2$vc^lI
z#wbx^kEuqBtnoT(X!>rgeUI&m-TA$PpBy-FKR
zo!zR=A;PLFeFN{#N`N_eGCF?33#=ER6(`Y&qTw9Xa4nU$Uyzj1F-m|q!a*D~D4fyZ
z7a&fd{)x)3R3Xx7d2c@g8qE>|#G7M~)s=of2XVUzM6LiaOM%b@h`SYtF{1o+jyM>$
zLX6UuKF5Pt>MByh1
zg@5FQeo;6e3l**2LYjy%a0N%{=7i#aEPhE8uSh7qLKbfq#Tyff@0P{4@!~V5@#1$f
zRt;rPXW(w9cWh+By3)Dl%BD={K!r$cUmAk0l5|bi(IgZFIsj{QDOEU5{_Gj2t*n
zSC?^i2fzT(%=EXmVw{=Uu3edRpYDoA!%4ffT`y)1+&P?DL6hdJIvASK{(e#@hrUj3
z|2!#_){{hS+xO(`j$8wmXwVXwk09gmUY_UF`zRd1SXdb(`JP=onT1ANKDEwJcBa;O
zUAC=gDXD5r|Inp>kpE)jeE{iza{zfD@6_B7<+6!4I8*?@?3PtEN%cZ~Y6g6<_uW0&k
zm!AFrM7t>ZTGUe73s;*E92}-*s~*ZO)b#7JHPFIVbmi_R$;*rM;Cl_2W6WE-5+8avR$UbQ>AuhVk)=V){phRG=dBX&zDbvm*-%fTBhb?IfCIS!{j
zq<;?ftU4IZm|<;1|2K}v?;ZFHz<14wwjW7aS*q!l4x%PVoVK?l+s+3*Hfp^c!mamr
zB8Sx3kI@1Y)87QSA{+k^jmC<|uP}Ug4bXl9INU^t-rin
z;V_tz{|=SH&%m^70Vf*Z`-Rdr0dMf&avl`<@z^%xF&8uF#?r{FK+}%1*_cDSYx2
z&@5`LKpuRr5l@mFzB~~Ef6cRy0ZeH?D}lsyTHIin)%CNh
zvCh!iJ=ul8z?EIE=_qyS`-m)du#YnRl*q|{k=SF4S}#Yl=-&3|uts>l*?5>8b1A%f0sk#AfaehqQKYCVDB$gr8MGNR?t}WEZG^DWL#KDc3&j
zL-J-yV%~oX4({O`5~@tCefH@POm3C((NRnb9{^b|-Gv6x31d(I+3ZR$LVDo0nEqOq
zr4;eqMhJM3KLHu7JvBS>M-dr@zH)YReINND8iCN`yqcao!A>04wbVh2HgjKSY<};M
zmhou%XaEfVWvbBkJCg=KrTIW`t3=xok@e6;gQrI3Aq|z;BC?aG_70AST!qBWB;Fyl
zf#T0i;)?$s0Z+kNj(Q!vVQMrca=A#1h%7;5@UW?Uq!>Aoi}58hV{@~S>`W*-OHR34!&;;Q0R`f*|(u3)87g&Z~rW5<+mfh0nx#^oK^3Hvw_vsD2Kr_w*UbpDMa!z1TR>|;X8>VfaTVc
z0Lmht$PZXtSU8CDvU87&pI>%-^XTBE7(k~h9nz<{Grq4+ozRNYU
zHvr}LT+ZNo`3E%^B$AA=?bl?p>nWB7IZQz~jEiN~Zb744--_%9Ve=1yE~7f$36Ide
zV_iThkhH3-(>A*pIqcTALdhKm!=odgpaE>>UlFJQlT3A4zJ+SL+b5J!?cH4eFVVUl
z%69w>L%iv5FNmVQB{lO?<(w7C`u>+EJ_$f9vIjE_ctbT5_4I)Wk@?7`WYqdRDr@@v
z5meK&(J%Yyq?%>;jbl^;t^mbsgfRyi!c_VRCmxsDN}I*FK2Y$go__L9g0Jh0u=W
zTIVY2h7jSIMLlb&S8f|tttLDlBO|Me7BJq`FzMx1I70|4Fi&B1HZ83T`
z7cPAVPMm$AO+rVr+Mo5P*S
z*`=di1Ds_2GY~zsvm$$R_zJB({k;c3s|iEjh-PSeEqjq*ZH&$CO-W**oY~bW!&qqC
zY(E93S$2N*ceqlvmD$>pNiexdsHlpdxr83OO
z9U{YqjHjVRS?8Ijin7y#t*CKEFbMETecGD?%Gbe{P=1jOVV@_$2oshe4~o0~fr@BH2wmb+0S6VepRi{#xjyBHfW%R$HF@X}@duYg+9-*Ez2_Yj{gPi(ywJ*4u?p{abrX_)VhC9pT((fhZS
zIY`faYhdVexSXbJTjbZsS;Gr=#|lK{g`cU)aE;#A^ducq1bT%S@9IjAd=QPg^isdR
z1pZf9b!5#E0ST@OLE{p
z8N3-;YX+wAWV%(=eS7dA+M>9Y7ynV!uHebj{wy1p@?^70PUp$rs^ob*dGcRmH(vo13ttKM6J*9
z7LLCn%lbHvU&`a#c)W(kAK>vIkKf7T9f)hubT2vwbWOhk-exyS&};Zl10SLkc8ByM
zUo&$zM$^9uJ>~4gvJ+cL?}S#)D6R}ool#sJ9;-!%a|zvBTuqbQ*_kzXB`E(Ujj+qQ
zI!K+2j9UMS^q@oQEP_DpkIX)qvPWt9kQRm0Jc=}~6}v)myzgIv;?4`Pzj{(E^Owlv
zqn8d6t!;%qhCiq2yENUINgGRJwY`o^U}7--K7yuO?k9|}hrxYLV;vN=p2>MJU$rfc
zYg;Ya&UgZCXGa0_`m?lXFZ`49C`Y#pJ&vHW7_xfi9}pePAc_JjT06O@EybZf$QRRe
z>#69=MeCx-4`G$sk3>ULG4c_7*)%wjh;F0NVfsV--sSBLA^4?}dPEH^nCdvoD;g
z?Jczv;axNLg{zP#q=Xj5TCB`YoEf3wdTAkX9BGUQW%`mK4c=CX-nIoTN>}x^F8K6j
zisIu4&2e?1ei!=UeoHR88(dxncpK6Ac_5N=Mp0_$`w+Ufi9}u2{Husk5V~tnH~<^O
ziZY%$bXKsIkQj*yb5LP$na0td12KmmL4t98Aq$^C2w^=>^Pv;n>I;-$%#9O_c^VOl
zDi96GpUOqf;H7|m2Jb$-jPnave8eL}NTG;EqoN|oXqEZtJe~=Q%vac>y*@I8A9cTk3)?%kF!q`?oZRQ#cL#A>h-h
z-j)JB899I2uk|ws5KYeSa4`ELw68+8H8$_N9hgIE-aCj7_V(6g0Q^}-$CW7BTZBy@
z2oKbNLwd*XI%v-Uj;sw}qt^9YLi#aBGCEEqX!=5~jBQctd%WL1Ok7JdRn6e(Azt-a
z0*hUOF6+Hps1eZY1E~RJ^#)!v4^znctIe|PFm>Q
z68g3^lZDdTTdUE)@*#S{B22ejMb)AVz5&@cIY&N=Oh22i8S1q58?#V8a7QWkBSBfe9nI-;)?ZfvA(bp+V70^fuzt0
z*vbf>k4e>yi8BbPP9FiOMt@!DzkZO!+dl`R;Rk)8;k+NdH7j`tI>%`8{uu^M&WA~Q
zVh1(+1=LVD1JKyF3EJg=HV7>*{Dk`mSw13Z%cWdm)9ACv4>>r<5S+8~0Pp8S)-G##
z1A&Ec`99JnuLxkUK&6@g2Ks>Z`d#O|YOwV;hTYgjn;2&SQdmGrQIN_Mq(Qhi!(cVa
zwa#(~kI5b_D&o>B%uH;|3gS5e;$!s^x4!`ej@#gLtY@OuVu8}(5Ox2GgAe3Ks`4vF
z<&k@fxrP)98hlRlMc?8M4H>`>3B7bOfNwy{t$K!LrY@{nI$Ru)yGi8QcwHlRjmX8k
zBA-Ci4c~y)kUCRNBXTewOgWb$r!X!DN=Vo&0}jSJpOenW!6DB4mbg~fX_#M6IZ#Z%
zG~v{SGg8SFX$+X9^!Fd)ldIG68$?WY9sZ{@lsmsc1%?Z{70EbV_g3WBpOV4S*JAkU
zoVN36N+}Rc9E84_vVh%5TIbU|E_+xYiaGgYF^|jQ$)dQ;)SAa-aV9S|TVFLs>~!6A
z7a?!h-_ejZ!~U*x5QZ`PJAwn+A2}O7@R!+NI_!h}tpwl?GFHun?rwi_BamYm9W#&+
zRu}?S$qg{K$I#~&(=0s${j0Ux+_M=}R_7h|~G
zko;_x1knCuH|k>mmhq}=ohsAx*Pv5w-bz&`%m+BLPFDJ
z>!%GE90cxgXFExVQa@t;nAgHW|8(+6BUI2xse-}_F}2jpG3GuTB3;cFBXa^W+h{mu
z-I9spJ57HB5)?_n{;u*AjAJ~$r%w*qcxQVH9GufJoC#hUPWrEj6f~+;7zH%`xfnHE
z`}S*4jn+KpqT1k_ntmx92dGZy@PRAFp@~rDhW%1n1*_=zmGIJ;1YhV@D$D}zhR&FQ
zLW5ogtf+MtM{0lM{$tNl?w^ofpp17WNyA5ysb8h{4Wg4K&H8dp8uRy@u;TjXQR@R2
z0e$p5h8D_n(hAzZDg6c?X3RZPwuc|*l)LnoG<^{a&j-YWOsC$0_?g9o@bJ$EPX`+=
zIB&aC`i^op(PLOganaQ`h2-Uld%L$_>`xM52q
z3V%Zf%jirua)T8-v>4{TntWkDI9Sf_N36Gr61(j!iqbwiK9jL
zJBRlVj-ed_XICY5ntH>QGGJQD_toj}6DZ&S{ukJJFXSxlviuO3sWWz`{t}`&P@M|d
zf6O4|*GIHmPP)QS0vj
zK{{9ODOS7^u~K!c2e7J;+d{tpLO3rQd}{D#hHMDFfCj)498R2knCf$UjGUjm{GS(!O7?b5n
zKqJ#{`PGH!fv-u_nuUy&SOT;Nzzw6usju%-Ffv3vgTU&try
zqSCH;!<_mrv7W{#u^*PvaT1DY3>T_8*mk(%pj<)iBo82c_u;ZT96dAwi3v60!gyR@#!U-Zpt<3;ZqDLTLCMw;dbg0aFeyp70_iEoK9hDrZ)4hp$RU1KP*!I!Kn2V@CW;HxTt2V&`Zz-UopnA
zrXWkZ;2$_ry*O%ZL=%^e;nH%qn*5G>lA5BbqAQEOh3hO35XDCDM{M*A6#zgC-fxg}
zc8#7s_$WH+flVJ64t&*GhR=nQF$peNhgMzI3$CEKunM#oggUFpwnw@$IR8R)qX%;h
z%62f9MAjx&;51?9r$(*gfvKt26@-L&r!{_EM#?=h9FQ@SXvvIeL0pLRpCp>uX_U~&
zTt~^Z9B=dL2}iZrIX1qHV=*>zaS7_C!<<99<&m3FAs-i|O0uyVvj`5tOSl*XwVKh<
zLp;>ad>%9xhUkB^=&PDO=p6nzSLRNrey+@6a7^0#<$Gh=99Aenn@{FrCv7fd0}_C-
zk-Ec##9k@Jj6H6c72Gg-S!@4mQs`{t1Fcth5uFYrziS>@mQb>$Ujo}ThLk#;4bye1
z%YpUaY%ndI@Wyp6XV*3G;NOCk!X*jnch$x47I-cW=`%X)GBt1&BGUHJjhp-fksrSa
z2lS>rL@K%<^(|wq7}Z$)Nu9)v42eJKuht@Tr(h26mB>>qJGiqS(<
zF#BJLoH|m-&O~H<12wMC2f4efbl)!j&t$&0lb1*rfXM3_0YHXfwv(_I#;$L`yC}gl
zm}xl`@WazIefwH8jauJFBdxQY5}1|bt;KxxKTbhrJ71Zx(1TFX(7Up#a2)ghOtS~e
zv+ZrvUQVD#(5!QNH|6Z*ZY)#`^m>Z&OFMs}+*4zh`8w)p=4={Y_#7NkT=xLdUFpBV
zY@$7-PU{L};65`|(~n?xK(~BG@fq_{L)b2dvI)>QP=u`!%rfEYc0R3KUZbOvc&qJ{
zpV9FuHcqjfg?Y2nN<`0C^)tSex<9fEb7J79Nbva%F3^0B_roCeub|I=*WUm3?$}h2
zjty{A_C6cvO2-DrP;kV!z2`RmM5-mUTFe9Ewvgt#TyN5xmxh++jOC-rXcT1wThKUq
z6Md@{5$vaf78n{$nldN111N6e?jUJ08#z{R64QI{z-5WaAJdw5h$?@ZJFV~A0Snut
zdW1YBSe~uu3d-LVzF=>AHz08#;Jzd=2wM}K*Ors?7_4a_+;q!}1c(egAq3}Aqd}HK
zpW!MfG~J&L=$#zz=!CzbChzyIQ2!TPCYI1+ho#4
zEu%4n71S~I{^>q-`<{9_d*f?c_;0kXlKlTB78(7t$av@LEi!&3QKC6{wB$ZA$v|lW
zt)fl^PEqTrKoovxcvRGS79mCMOyu3s;JCo8+&E*VOHh%Q(rFCbIT=f)$3YFq$-kF)
z!4zL2PSaBa24tc9h5noeIF!*{GXdL5fW$aA#rAPs<#VJShY7jNSeL=5(3D4_p|1vzEJ>Q@g
zt_v^B^n(whSU?(47vj{8ZCIXX8Y)&@B&w
z5Q68p_&l&VE#^OSS$SZTAuHKh`*qn47Cr@<;uLDtW$j)aGm-9c7RBUoQgdFTHlbkz>AAoml2YB|KL*GCxkJPrin1cy3v)($FX?RDgV6@5HN9S?_sg@*Qz-`^%*!VW@?cZd
zc8&|i7B2032gtVHm%UnuL3!Mne^Cq3HBH-F)PiC*n2+r|*ztp*EV!~i14+qRq<6rp
z!Fz-l4c%r;48>bZ3
zEEs&FA`)BBWu9kplO5z|fHxuKVnnSD
z>Kn@H9U2Gk_Z0$3U6-L~a3A*t&1l@mUMUd!piH9SM;dRCKtx&l
zjo4oSC+#7;d;#ctBfIgM2Ko#S^bpqVH|8*$|I$%#_#`rQoxUu*C8~9Di%oOgVlP}2E#Mr9BJ}g{h+U&`QTmiGos}AVIBWzpIZt7L=@&Is*izNCX
z1KJ9;f2BJyGhYmW(2EN(544OD>*J^eIRf&q;tylD#7@eOj_fGfIfClb{&gX`0kg%n
zLu)pACyLbnflalrsTLoe!&~qTCTd}9i@YTYV8=xt4%MPp6mTj~3%e@VEDAss(E{34VD(*5
zfTe~g&{RQK6mTIRP;mQ2i)Eq!x<|BVQ3b_R&>5`k3|3?PI2$AnkB`g+BZcY=XBi7(
z5MRB@fa?b%oSfDmcN-QWIOc*X<%R-gF1i{EoCOh5f=5W%J;N6`BZ*?rsQQppsQbo)
zzn`=`5SkdZPC_B3!`YCRRv1a_$5HLhVom$B|McEZ`=k2?T3|LX{b>gQb3^XQd8W#Bm(
zYlPpyzGzX{Jj+PRg&$8FrJa`jlrN7{qSgtZ+MpG{?+y|0D>-%h35Jy~pZ60@;LigU
zy2TMMf>QplHX(u;?=oJRh1~#t@2?SM7|2~{o$luPnBm)Q00OE<&
zDdY#*_>$C5`ThXX4qQuVKc64qVjcF{*}T=QXayL992E5IoxDBWyS3a-c{5XSL=bEB
zL(wXPR)hP1q1L$=Udl8sVEgwLR}Q0oJBte=4?wQ@cDD}`1-CGgs6e;W(s-1{m+NCW
zhxoqcr?|TfE}n`@!J%=$>uIQPjP3PO8rvs8NZB~9MGYZ$$gU&jiEcyv=<8F$A2#e2
zMRPimFN_=kCg`G?T+l)gHVosDel7ZeUqCH{OHyQ3EaX-~vlP-=4{PuAX&ulkAN>v!
z0*r=sAx#4Fdc#@NT)3ffRRWHyGt8n?npR!uTbfX7=B|(j@xuWTucXTZ5Xt=*YbAh3
zt;2u{Hp$6S&Y_EGTrHoC^E_ezPXkY>Ra1Dsy9ZB2>(6hbav!+=@09N4FQV2b&jg&AFN9z(((j>qUDi)X)>omEl59|y
zs=fd-Z+VqN83~s&YSqtBQyse%>17-q7z%jfc&%k>GmGzIsdZHo>dCz|SNy6f{j{4=
zMC*dc9(s`s1RNnqf;aq)bEH!Aiov(@_etYjX&VIjTp9{CwDlg&qg6QCB7MR4OE2eT
zLjasXPA$zmSBAj7!uynwN4VHJy8wUDg2z2)W>$_3%%yzxNrhV)&G;q^PAl?>Jo&
zsEp#rsO=D?wvR$$5l?an%IYiee>g7>jczs>9?=N@cA+dcS-eML<;dD3e8Vr
zdcJoWH#lmQlptaUX!VBUFq|!j7kYuo+jNqEiC^g?*?5AZ0`u1!x&*48&$0_>U~%iS
zR`f#av-9K`?wQbkhR;sUZ&r-JWvI*fZlm{lWBJnvhHjZo9YTTqn$w7jzCa2T!ZH~~
zxea54ZwlG}1sD*42O^0TE*2UE$U8{wX;Et?2THVgGxE^i=^YrZa;O6I2`YRFHVXAx
zNVbYD|B2c+pf+?7Bo=1vSA5)eoeJ%OE%)7nuR4~1UPTrBtd_qvP)(
z!Ij|@z%S8<2A7oSIC3kE;H`s*)_MI9Cg{o6!+iYoS3tXyS0;%{E#gPccuS?L%n`K?
zVx+j(@A$O8^z=_4?^x{{SE^3on3p+ozoWby+rG4D!4U
z8$sh>6$u*ptEBN~toj(Z54?{roEE2I%qJ};5YJ_FTt#_Z*2S27Jng3R%=GO58$Mke
zvdrh=kg@7c3ySno$3QnC7&q>{Sx>))I9ZC?P)Mu&yhd3iBs-%6KQTs-+$hWF*oZ{b
zdc7D)C9I6(%FI*HGMce^x@hT$JWFmCDYtB%1L>WQdjs%0U_f?-PN9uGC?U+wB($V4
z4$$&8mi_+8BAb-vew5%>o1UPoE~~$lP`DiEaFN;rB4R_I^d}0q9)Z^XA%{wHKBI$P
z3FlPovOab#wfzEsT>S8ad?b>O8+jERV+Bz(a1~F9Gr6uM*yl}O){f&vzGA`oCAe`2
z644*EcA>Tthb}mY!95?+%K3z2jOE#sBvCwzK*nydkEVRE0LOW4ki?Q1T7ax}aVRq5o!FH54J@x^Ek1vR2f6lw$uSReA*%8kXyhX;GMmJ5M&Xr1X1B1Q8-Y=WfO~=8AX|)
za{)dApeUSCL_>f!j*0Q*)V=`qp-`&f>Fd^KMBQHkP(s~2)HRDdU9R}`Si--wa^SBZ
zPf2Gh6czD%9w?*uy`2wHnA0DlmP;4xBXwWqh#a7I@;IZo&j&``N-#;dI*8;kjs-+t
zM{(v+uqlBv7hWCX%#YrRab_MtB+*_gKmR}mGwi9EysP4gj~EI&Z)Yj57RPq8VFgA^J)&ZtWQn+IU?Op
zV=#^q`LBKHrR7}jU|G=;@qqyY^b0N6mpQ7ND^1;Cdx^Sf=U{;y2Hj2$0ck*}`*|qt
zSAD-QhQ!GyYJB0nF-RtjuMB$R;}iKn9B~7Q
zAd@>8)%?25GPoMz-7mF&X6G&!+_X!`5{z^M2DUIJBVJY~;^)xxz;6ygS8{n_^O@?VQ9C}jJe_e*XGJHse-<4rNhF8k4M22=5sxeIJ5wWo{bjfg|>`&E8mhBG9
z@;({vkm21jyjg}J8P>~C!O?yqVoPQCZ5du8!|%$lTZTJi_?!$6%J7g3hh&)cn1Fkh
z3=3p9SB4cb49W1jGTbP`XJoifhHuF5eHpT!3iz2aJYR+v$#AI*12P=_V^RMBncgWw
zyTtc>nNE3J)Eh6uLK!ZQVVw-Gmf`ngxKV~ZGTbM_cV&23hF{1q^9j+9O@?+EUM#~(
z8Gc)aH^}f#8E%r{GcxR#;gAgf{-}WeM;X2@!)Il9pA2u6VV0!B7m`kgW!Nvn9vODa
z@NOC2BEygjFPGsQ8QNv|f~4;mGVPcBE|sA}hIukPO@?3Wl;e=$0U7R;;rC>CvkV($
zxIl)5GCWs?X);_R@i{^U!1Os|9BYHIXDaMFN2G@mLe=C6nXl6SZdm1RY^(~ogDZjp
zQdMm@5b!pKRNiG?_lm{d5L<~b;H_isU~6-2gOAnv@WU(kh2l`aUF)rJ*RGI=@OD)J
zuRkC%8iKXHfG-@v@3ZiXs=9C}9Pk=700R(0Rl$&eQS5H6^)@zoJ*9zw4=`kZdETpm
z6mS&qmwQzUk@g1~fU<%?9XXq?b~iS77Khv+uLQ;WSMUt17hqGmdLjqtEBB^7|SaYFhy(Cx{bS)2HuE!DoNJ2vrgGQsO@Je)Rv!
zr#HS|qmPit&y=B&uOk0j_$c@ZVcYkPjc2OFFH43t8Rp2)E<*yReoGu{308=w?EyR;
zR1zy2jrdqRJt*r*C`(7a4GX{nC_5m_ki|b$Qb3ek*WlN#S2Q$Zvq5~^9#56W*X*sT
z#YB_y1V79U85ia8@`A-qeZaR8MKK8%^%5n#Dxo1J)bfU4$Q$6oi+K`mPRJ2(sxXa=
zO312e^t!JW63;6TSdv^y7!kw``A0n94Tc*-RD-b~>L;e->o)lSKam7X&;ausFsP6N
z!&g<4dj%0oYJn=RPy?*0s;Ew@c8oW@82gd`+`-g3Sw
zB&*EGQGboT%P&`d*NwT-?Yp^#AS
z1ktG&;|O{i>kRZ+1yt|ikk?=44b@*3aQpqTo}_>wN#SOZ!-3$7_|)ufGU!HpFsG@U
zbeyWs$La9~LjhlFl^@7T$W=|@AZi*CsTz?ySFLn|))H@}2nk^t$D(j^GhnzNE1Iv-
zBdc+|@aG1o$SgjL%j+=;Kn@7d(G3*+6#gnqlR@+2GCv59H9xoK%j
z4C%MId3Ih}+9EG3Pjk?=0OFRX;2kwTbFzs+lWcX(9OU_vY|_-to<&p24`Ig?a@RC^
zFBV-Dc|7PtO>bMsr~cx?%6f0JEmZHd3G;yLS|7y6M&b^1Do|py!k=xRxYuJ_*$}D+
zydVhJ=(V{6%fn6R&z5V3Fe#bJnGq-~)!cG2nH3T6q^X**~%k=pa#iuW7;B
zdN0vWc1%cM3iz58futOIJ&G=q#i#`_Z54sA*Va&HMsB5>#vi%zMz&N
zF3e>242n*&VOBOaG&O{{wk@P*Fx0<~xIc4eZ
z%}A4h5aQ~s4Tl6R$h`ou7adC-Ght41h{334Vk-aBHrxlG;h*nrZ1M#|=YxGses`z=
z>R4z_C~LWb$rBSPC@nO4wBb|IP*>;00tXDfNaW;#Q{>O&Or+Tyn^;JDe3)vGB)BAG
zMjD#Dplyr|H2DdpO-MfRBsO0YwSW9%k=W$giltjf@**5K5J}j8M9+~B3;>bFmF`w$
zE@b~#Hvm0SEwoDFlq1Pjy1WWbCCoz5UFU`PsZqxhl*XAzOM*x)fTWB&25-{=J&
zb0*53o8Y(jJTNiB)E!NJh;{7>cuZ2T0^XzjK{c_t$yMr-s$r~+fQ*Sl{Gyp>GPHF8
zUlSkncp9j%cBJ6OV`>FMCIO>iG>4mNya9qhmB|k$f>R$Jfml&3Tyk%~9fEog6PjxZ
zDZ(**bd(=sni2|iax*w)(yuYGg>(`3Kgj+=j`80Oeyz;q>JPfVxt1207&PXsM`#25
z0jDr222A1@e_;VRfLIG?G|uMSpx;~DP}fk)2d4Z&MPsar!{jU}J*)ggRc;i>A(Ok8
zLpo9@T{R~s*~rQ^~PF*9BiQ>#!ySHZBjDF^!F2xDTefhgpH23NTe&d
z(NQjl08dmkj2Q%h6DnBm3pYx`2#S&z04Zn-$T&G_@B=)JaJsmNaqk6c1)P8*R_je`
z2-zSUu!)?na=@n9&dEx{B6=H2K#jx5h!LLXP9#)S6y2pRl1^y6vqgd|%7F#OC=?vB
zq*HpdS9E;a4#mRE?Y+sLvd3_%z}E?luW&XX~buJMIo
zpF%#7@`g8ODCJA5O)${bCvtMU>#b?$<3D>
z1`p!IiOFnG|JYcPIMqTBV}7u7qlG#VIx|tEL<5Wlx>SfIR4>=Md7?jf7$_AwnTu1C
zyOoyF9wo-8AEqU-Ezc16i5zj$ewN7+3-8gqAy7)&USw9`#gh^c4k;;k(1%oAlh+mu
z!*mL(iz+l9sAdiRfL-%)$iI&}RwiNPhCIev9+Try`a5xbyGTqn!9A|}I5wh{tsz8v
z57aR!MRy=TYg6cXpy*DRbc7BF#J3B%yFjZGZ<;c;#;QgvE8zfPquo~9S_?m!+Ju*j
zwL(nUkUv4Jm7Dbhjf^7sC$vU}nWWi`-NI&D*iS^GreIKWE7$ju<&sOGK_C6|Pw
z^$E$JBoy=uCJB)tjp!u1`1C`AwmwT)V0
zRuXBClB6^s+5~2Xz$&>(RUGF-#KvgL;lhEP3IQ0a1=^+HYYgsp(JDm9IMI)51;e?b
zA@Ftiml`WI?Z}TZZy%bJz}IlcL6b$aX#pW$L^i5PoE1rT5{oTN!)B;nh3;2*+vfdn
zr9Xuq74!TQ<{~uhznf4$T7S7{oBkN8OX@5Pfapeek}Q9HokjHHLeF<=O(?`s50hRk
z=6PkM2`?ihAzf*{9)q$lGziyPYPkiG$XlLgY@>-aE@qkBZlu|#Hd>0xofD;AFzC(a
zzg+oB*(cp_^a6{f7vG>!%(ER`hql|O1KOFD>n#pn0G8^Ewgkvms*b0SJki5CIzB&V
zVl^j3yfytA^6;ge2@Ha?@W!~5*G~rG*t@+)>k+Pwg2(hrXcS^&DrU0ccGu$MfM`am
zYa)!;Ta%kzU$(0z{i&E>@JS^$4ke!j<%J3paN!*g2Sb>FX@*@kRw*VaBNL_cGj@u@
zSjPv0bzztsk}0SirOL@c<9b$gMs8n|Kh!Gr3yjUp7@pMFPXpYucpZki;490#u+18!~tTywYqX`%&*AV^w`x88kq
z15iRkVn?j0*wM+Gn3gsd#n?^=;vgSdvek#B3l97sLCrWBYn&uD&dJ#}iMM%J(it;E
zGA4;)<{x#Q6;r9S>@&>3Vyrs|$D_3qXFH|{P4NHKts!oJvhY2uZCKmd@8R`1#QPE2
z5dQ5{##Wxj*ll=b<1LNFcuvCGOerWV#ItxjW0iQ;<7rsTSSy}BJn2jD7l`rD->e?8
z0OrYzb>nb7`y9sZLiQ&JduAabKUM0W>Y#Yec7{hJ`?VA|N#r9wcKpI$|_iDNmh!
zlKG=F-(E*Z8|Ref?BttljFF|JJo?BzGo0FEh6S7-=d)x|f7(i*L!kFvEAY>aeN~Owg
zO=v26g++jT2Nf-a+Q`y
zMCUpeI2UV1lFgEY#{zbO|6(Z*2Jx!+uALW0)aqQmP8`Rx;~e9&3Fgfv?vz5_NF~U_
z8bvQRZG4mYpd&86{mN;tz?0ngR>C8PMG3%#R>!n-nog{MI~IR|K<#3ioMc)*5a*z@
zs6o){$Jz(Ph1pSdc{
zIou~$;SKv4&fJ*?7sK2E4=WbZi&aPcO1q$))4svUei>X=Tr9rC`B`L?`
zA5%8TMBpmeO0
zVA${X1#nhKs}V8Ne7MQMC$VU7{Q3rmZRRGtJ|QIUY`{q%Bgq{in>5Jd02
zMJ1XN5{%z(Q7THzDLUQY`y9N&*XsWmJ@WoAO6pRo2&ABg+;pLV^o)7+ttVq@Ji*R@2bD%-!UfUSMd7Jin)g
zqi@AGJ@lcwb8N}Ha%K1P=yqs!qu*{<3I_fFD)-?sohWN+rY7{89Mci1yllIj`v`X$
zc`a;pHe>XYu%ga2be)pznZVfl7a4iiP#z=iAQ^cL9(Tx{J&ot#o*Lcx%d^{S>tK`%
zkxr#_@glrBQCviI9PxRVQ=S%==j8Ru>LEjREw*Wx(FERGO%RyNmGA+Nd3`lYO&r
zD(hZz%{87H@=b=;Njd~Vwb{-hj?a;}yvsR0Y`v*og{YSrmnZ6FBF{b&{j>BrNIdL%9&ill)6&)Se)1IFf2
zT9Aolz6knKJD%qc5bwOdb!dK0!338*>^DD;atyxMFD95iQo|L90
zhZs9dboR^6hX9l62ZDjxZ2J^MSvT?+(L?0%@lyU*eUk9^CR3&kvyY>SnT-7z579O$
zEt#biq_DK9DSj#&LjG?~!oA{Sl_CEC=2{7rq3(&+RAvp0WLD1zW(5rE)DsUK8ZZ8q
z`abyQ7oH;ga_aI2h(BmbOMJ>tL1nfw5&^Y#CEW`IAN_v5_?>Pfcd2fn@*i>S1=-hdn7hnWYz`vGheL$X6^^7d
z`cnO^z`Xkyk01R}m^3Vj4J${e+DTp=l~(yvd|BD)Y3%fh
zbar~dXm7(duhNH&DWd$+?y+Zm<$pZcT%?)@8wg~*=)4(4($eb64uY#ROV{`wi
z-vZHZlCmomurJ6XyCQg(>e8%&P#3eorTs-l0l&qe5dI}p|a3ULhr;yQE@DDYU
z4&(4cDNI|0_lxo5Y!zcQrv(j#CB2_1gIq0#F
z41cSKmA5*v{bg)ILS`SLI^vVk13wFdXffq2MP3{7x=nc}JjvJ=EH
zpZ~u1D=72d_kIN({`=mqpu_(w@BJPgUfx=i(E;eJ-tTwDiV9K=yf6?RZUv9^-NpeK!&=?cf2
zCf2TCH^p)}{az=Tm=kVj^a$|ZNu)Kn^*Y)B!Kz{$SYiOcdn;NkX>O?XdAytmIN`*e
z_fmGc#BIK}wZ@07cFvJN7@JOMDHvB3-CkQ93WRGxuZu_q!noRBMch9q`@oxR>>ot&
zmf9*J9zWp$f9bvKlT5NiJTBw!0XRL7Nytr*O_9AXZpDUjQ)x>BdU;E7j|dECpi8a^
zip%F)5%x==WWSYlFT*P=UcAkRTRL?0m^p|hB$-Q^>BRyJjt(V)YzO)y5drs_J=iT2
zSaUi`j31W}1%0rqg;5GItinKP24F{#@TuY_RFX8epj9!>7^`>%mzpci;Noyih_`t`
z!o>+2jt{-;Z4RTv3!?aHSyPP1a|2#4dra0gBu`CFwwcY)g)
z6M)Be#A+_Wt53!BB2d6P$LE8-nJf4&-q@!JZPKOJ8?y=>kOp&lF2j{tDJ2_0Nv_`b
zI9@P~Tu4Y&tk;;>vt{wQbLq|_R8fGGl9GhB#C}bh^S)U77~RE`VzCyK$DV>kN)!4w599IQpGS0>|&gPHwE!y9c*dwq9qF|
zoMoj`3hW#t&REcb-UbXbu?u7IDlhv6&JexzRk$cY>XZ7P!HPhP$*kXvQ0TTnwkDnwU)y>8;yc)yb1FVWGI2rv>
zJ^b2+zah6Nn7gu}Ikz?t%Ed_?nbq9JDY;W}#XvqzUKD~;;in%Fk&@2-L3zS_C|!r^
z-T`QnWD81`HUvUpcjJ;~fQ85KN6V6Cac#h3DB-z$@QiJ=U_uRPPIg1&SHWx^Mji+7iRYdMaEi9r73OT*qkYq>XoL)9j_KU++<>EYYt9}OM)
z@j_FskU(UzwBf)+nAQpqs0hN^=%pCuSc$NdqOz-@Cg2XVlBfmQt;1=tE10*Kl(hz(
z%esMW3Efy(?hPm`=a(*8Q0mH`Dr9FC$yF`=n&f{!|7i+LUH@M0y%+rJP+$I*?O@&O
z;wNJW3hjMhw*{Y6{*5n@lqo7Ve-hKGtfy6+hqtK=8SXs)*uTf3s+GSa%0J5c@_MTN
z*A7cW&CHtw+;Y1J+a|{MqqbbI58h@Tn_uFk`m?_pU#{{K!+M!6h~dN0xD)jpu&(xYoS#<>hg}j*i&exr)p$zR)M1z%n-vE5gur9SQM5ODh>{54bJ`#GLI?d^-NS1spjTW@@RA|6Vg&YB%x??0_i
zaYlShz8#N0J(53(^VO9=kHqo(yXoW3*MArA|6kj;2)^AnPlU;G9Bpevx=@zCTO!go
z%ksz#BHbtXxG|x<<91OVk>xF=BCX)>l=GfRJ@D9EQLenGzI#Mkll|Lex$3{WO_W#4
z^8SQ!yM*hP$O_pzw_$l~Ij{mFJn&|f=)_+XGUp7nhr^cUoi%4&g<&`ty
z%lobu>0Vi$D(A1F&!Ia+d7mtQUBXx8j!sd2Xr%~oKFH^%8ef~F&mq|!XYagRwI8}!
zq(74FJ%#b@hprRpBeH!~0{-2yJXzZ1!i4f6iQgL8eotcgcLe-Q*}g^Mujt2CiSis-
zj=PQ=f2B_xGCz?YiRmF(Z_k7{4XExT3IjaE8r=(|7wW)ww(U^6y2m>sOG1FnK!lnUwJ;ZJp8BS`FQkC%>Q@o$-iq4{#|{q?AcgpkCi<4
zNq<4vv%8B0-Hmdo@0C5OaENkc&-$c2&yw@=-J*DTUvsNS=g9Ix*}s|(j@6<(OU|dv
zIq~q@?v(9id2d3w?M6|aDd(F@;-}!-q&+K?{U1)i?}!|qCd+#g#$Vkb;8)7>K56e1
z{5Dzs@7hB}U$q`n_D7|aJXSs+@KOGm%KxfR`8x`p{ayj*cRQ}fvCB4-A`?~FptQGhtj;~(AQ~vAMg{SK6
zl;ctSR{38UN_r;Zvs%LQ%lYw@a1tu?{j2yS@JqdKqwrJqQKeP4-Lk*Je-huyg#Hvi
z75pPoURAn0q5kpY>yX5!@}Ifz;hhT)^0y$&
z!D3?z;uL=KRJ@agI?E8=i>Czf`w;fwsYjf`^{3%3EaDpxTJeYLmLZ;na1S0o;uQV~
z&vl58#rxlr@pK?=N7#VpF~l1YcH=pMIE6pOGlX~#!b5m6H;V9M#Iq1Tg0K>QQt(~W
zp>Q=GhI|U|!;^|Qg>T^5gF5{PYw_;I>xg?0{vFS4h*N08`{>UgPN5Ue0mPRf+-O4{
z;81u7j~#Ie@i!LOEW~XH4~)mXV#MjL>o@Tf;BM$Lg!uakY!c#q2oK_^M4cgo^xk?k
z;uNN5W2~Svh3De=JK_{xgeQ4}2>po9LY%_YcpgHW!rgeXP=61?2Ji?R>!*bv7#5xZzIXo5J_oa`Pv)re!A#r}Y2
z65?3HF`W0Xa>OaT1aDYn0)OnyvD@)@HexOy%*8E}XC6df2yex+2l?iAMa=J!nBOs>
zcTYCp`8(=UxF1g@;86GnJT}BB{2x4a#Id$xuj1K=IQC5$&Pdoph*S7uJXxR*&OKNZ
zPY&Yrg5&vkY>4Argx!rN6>$o2uEcDRHwweJKb?uV`R+8`sUBAd9?`vOgp={SjyQ!i
zcz%yKh1d^aM-Zp*lo`Mc@k)fx;YkHd>}Rk+Joh2r{9XgS?{MNQ$PZ{sVGf?Ph*P)(
z&lbcf{3f1f5I4W8K<_ZTgy%5wDSQnN17EQB!$!=;n2?WsA~q9GKjPT)W!GJdzJLRT
zP6yr(0o^JPeu^iAeDnJ_^q$TEJRa1c@J&3|Ax_~tcy2=+elhzQ9uL~W2WIfoSvleq
zK8{C29r&*7q!Q>(#Np?&>3GT!#~vJe3y%%;-$i(JDe54Oy+O7akA^t*;aKim@Dy?E
z`LRdw*b&G6AbS%}Xd}jkaNa!74EY+uAL405KK3Bl@9-2N-+cd{-V4ab)3;TG7bE_A
z#3}US`3P|eSK>)U+ZKeEIx$w{S0LPgXU!JCNBFHvK>r6ZXAo|{L+^f2IBh=WBjOa6
z;;BTO!kh8bBi@1VFrG<>Q+S38eE~j&Z{fKP`R^ipU;$(sar1jc^uE#3g&5Z+5z@OI
zF2v36c+k5aSCnInXiMQ$cz%yKg?Hfj2=TiS9>lX6@z)Xl`clXq;)f8VoxpZf
zgPw?g^=BA=AJ_&JkcG;a%5Cv&l2{TlK&qqihatcI82KZUkIF<%ydR1DHHbw%##n|U
zB0u!rYNfy#!Co)PTX5m+taf_M%1Vs!l
z`9l?6&%(tvr)T!~YrRwLQ>GM5yD)dkw1Vlm1^Knpa^2JFyt#IJp~vg>=6mw%?AMz|
zb`*sCX;bnGYi77}r+H=oLS2m~x6nJ)lk1*ZP+*@rbz0qweD{|_C~!}$n_B42&z+WE
zGb6X4pkPXFjeACIu4noTPl3C@J>8pM_~j6$)J?x|dhLbNa|`X>DY*qx^JnDFm{wDh
zTbS>$*G>1>J?;y?0z#qJUU#8;%GBJNx|)29!dsY&F-^-YD6I2Lv%9BEbI&l1V%FqD
z>du-R#|XhnQ>CwFB%cK_xuwf$zLi1TN2A}Pj7jJ$Otanj8mFiO=L?A#jXwi=PLBN>DRwmdXhg
zNevJaKcy3#_}x(a9iLVkU1gj-9$n>69WQ=Te0-VG4S5pXykcKdldn0KuKVFPx`N|L
zBOLo<=&9*>cKSD-{+a_#QD2}aP+O0yNMeXH8<+F0vk6{UfxAH(jizDA4+WK
z!v}cyYjCI$Kn)vu^06?=Jje;5SE@%J2OABz3Xc-h4YFE}pFgf%Jbc1~;No`^*a%bm
zrN|2aLJWRBYD~euJO-y0pXs1j5+84f*KGzqji!;r_fx`L_(vam(k*D`LYw8h1!3UF
zn2D0hG42rR9Gz!lTQT;joSIWuE{{#c?N}RZ^FL3oJhc1rKJ2J52RJSSZeCD0540qX
zeOb#Xpv(+5O=4#Ut*Dm;kUZztXh)+`5Qy((Xp5y9ut*2J7E1b%v=N;ZpD86IrwXwk
zXweKziEDYBsvaUa;>6o>!0v&N-`hB7~(rf$sKn6=Ti(Z2D>gKSgFrZt<^ZnAAI
z-0aw_Z7$zjxfylfu+f}d{qUNHyC2^9aPPx?4b5_N?tW
z+wI#6w`<$Ww^wej-|pYuvVHaTHQU#2-?Dw@_MYwgw)buC-yYeX`B2tF)eqG_wECen
z53PM@=R-XY^*?mzA@*?U!#NMzA1;5m^5N=-TOMu$)wY0c`#`sT(Cx^>L^Yz=mK{5H
z^z7){(Z6G8$B`ZOM+zUQe5Cr3`bXLxS^dbCM|M8a_elREM;>8)SSx|7tlQR|({1n8
zy34!$-7Vd1-M!uWK;J{%k?teiY+dTQtaY|^Ih@)W=v~bz-nMS_y0z=NIpurT?OWHs
z?$El(I{fC&`qcGV>uu|EzzxTGZGGkX>h<;OTh_O&U%h@UxU*$_&-&i=`#6sx>yNCr
zZLn|9Hk5B@+puOs_lBJtdN%ZJh=6mpjfEQ>;9c9sog4RU?Aw_7VCMg6@4BO+NWL`#
zL(Vy8BuMHZ=bQz}hzJ6bljNj;WRV~ssDOkaXOJMEfCOPlB9b$bL4rgpSvJ?EYCn{$Av>h78vy6S%4{qC)95L4Z`}$Y@0~EaA#~>pk0l~azXqcD7Rl*N(
zA?V2PV4g!Dm}d_e0)gQJxM-MW;VKX-6euzT6$vm#!{S5(7t99>BSWC@tAIK{exU{*
zz(j$7&p66L4nzWAf4UaRuyLPV_7`sJGA-uixwh1n+
zm9IRL?cMnj`w0{Xcs>O0p@%MehE?bK_8dVi@jXpta!Oez$S+YG=Y1M)PL^>!`on
z+_*v2u-?_zTOuH1T5-qvlXs^|f%Ua(8G42faX4!=^MP@LPu+J-7D~G`Z!QV0zUN{qpU2
zh##Z+P(v9til>Q>^|%;JA%$P231ieqHK%pC5Nu*ELI
zE6Gdpx`~|E{RdC&?x<|fW)V?ar+9SFT%lXRvnX30+IsXzEM0O6X;JaggPIftO3r}h
z#OeWar*;1E_77K}NN@dV9>0k_WUQZLz^<+Qy*m;9OB09;MkjqG+1N@M|Vrdvk17MF19S^5;c6
z-XvP?&*O!av`f^;;Jkxm%KC^)CW0a`>E4@w3l
z*%)029&k0WRR6$ZBfat}sf-3+^WrE2Cm1-&AV~G*HE>iVMOoCfJBSDOc|}#
zXPsZ;#Osri?HoE<@(XNp*rw8s*vf#gmj^Z)Vp6f!MVWLJ);mp5x;M4%_}^m03hZ>Z
z?uVnAnXcwm=a&~8Itixw8AXm{i6rm&MD1$~FksI>KSe)N4sye7CgIxxuJe@5y6ze*
z+CO!Eq~-J^#`aE1r4Y#-v|W~$n1ouR7!`F7?u0U%%W*7a_hvE3j`pWoj<2z1Mv;uF
zQ&wupx=KoGxG;y~s7(b8yyZ&_#l^sUN~78Q3EaK~7?>3mAP8e5g!O*1dHW{N-rqXO6RXyWoZ((CX
z+4!G^%)ep}L7U760|fX)5f=G3n=AwtMfmtZS6b8vu=^1g$N^G>mB%8+JGb&&2w>oW
z$pX@d11Q8I=LZn`iheWy$^s1X;KFtWT|1j^4<*;pl@$O)g&1^IWcft|82A7_7=yel
z3^c+2*)$JV^UA$vFZHQ485X@xT@fMonLm16GU%yGdwp9NE0B@%(00uD^SS^F`rH-G
zK_B^j@=>~pqk&-?DjiL{%xk|!B)7!B4MLI9aAC2mikZjo7w}gPyhh|8pOU=WE>6F@
z)uU`goTpv|VYswMFc?*Ng0USrn}*qJm4BQRs4940Qk!x$j?qVvCo_Xg9%l1Y-b1>Nv@f!QV?6U->cLN<5I~n2o)ST;sN6}r2WEQ
zsn8Nz{W{mE`Ifs|_w%z+@H}$MMQe)9h!$2yULL~crb-tn;A1V!%F0!l__wo*os!T(
zsB7(QxCa^)s2&jn#Xc9u9PN0ATe+LEw2S8Y7&yv}+QJzRMtLa@B{F8=fD
z!4u~Ma{=Rq1V#1`q083nrqA8qMX_E&s|*=3J|Lx3-@G+@lYe)0bptE^ISOZdrI7FI
z!%_@7&pfNQ^$RLmd*1O>UnjM)lse4~lRo<)lSzcImx_jO4AtW5zW<`U>)^)r-It~##Sh~~pYe(NZK
zV?g+uX%2>52ZAB`01?9aelyP)02*kNLGyaSBHIejuGffvF(nK?`D%U_Bf@I`s@yk%
zLdy)L0LTLW-emvT!Xr2k?IkD~YmBdsrVPx>X=`mpcM%#6;^U)3g0phlLx&OjMj64@;}=
zv(=LYDQcajHr9mAdIbo(_Anh96~*;^POx8CXXqc+@{LG|jBKfC%v7NWm>LVDBBZ3~
zHBSyQIOtmNT4`IPHkJP(m3ZbQDw#f%+^06pzHZ8z`PfipnCj~&oAha1zgPG)&2uy3
zJ=}s%vkx*GMdw7WPTZ~+N;_F%)HQn3{n36WOKMWSj=xh=gyd23=jnHPPU9xXB=j=;
zekk*gD_a>o-F14D#@YmEvU0D)^g4S9bA?G$y<`1I@j4Fux%|Dy5e&gD+i$zKPB{dP
z=^u#b9|}iCVP^JVe}qw;@lYCzy3V~$&BsC;VYR7!w0zYRBqFMPJUJeZN+%_9Qp4#N
zS`4T;na^ThtVP^)aae}GIu(;v%xhaEi`R*gZB_}r@$1HSu
z66z=b_tGB8Iy27u{^^TD1{?Gv{zbe%Ko=tNA^baZ@d+Z_iF4@k`w_YjE*OYV7{q9r
z;5Mz!w_!v80ipt(7*h_hF^`)ESjP-7BI>Y+skFW==V#Cb16~3
zr<48>%?fDVlMEg+J*zIun6N+186?qY#%#-XtXq9_3#t7chJ`$@AiWNl`@FLtLDxER|^6}Ht!Yj52)0Z
zzTS}U7)*cA$vf&s$>(8dfaUu-hzswPw|O8h&h^mRmxckj`TbK}uFY6#cF(U?rbP^j
z&U}rY)>L?sDQpB)7T6=YbSIVh1;3{tZPg(4<(Fe2s4B!lXIDqm6Q7ALEi{JSwNqC+
zJg{VCO?_&c%v?!!w^A+i4r4)asxOM0xP-2~+?0~~Zu%}~f`9+#F#Bpc>~(C#Ur3o7
zMBj(ptjL;<)H^io+$Qtxb$JnolJbJPBEz1#$#sOlr1PUs;}v}gnX+zz3J3fQJHt=O
zMVdN(?0f9=iI48iW&~M~AM4DXJbPPJ@p&YE@@Pz?XDf8)-JC9$`!0kuScQT5o&PwK
z@#})fknd=Qj0TPrl=eX24gxG^cc{NeuG{b=UMc}XUR@+avdr>wf-)s=3r28
zlFC2x>udHh#;%Ov-?yoy~1O(}ij
z9qc|1kt=)aUCn#;P<~C{z3>R0Jl5DZ3!ms0lM~$QGkqgPi
z%0Z=Gme9*{OIn}N!^G8SLPqOjK|&?dF8kgM+sPYlD^%L!$Bp_#%UmO(?vjfnIiyT!
z#1F3)yDDZ?son&fRW7@@oE{sg8qqYR&~4t%iICG!Pg=phqVYx<+-FHg;_`UKyinNJ8#idMlACuzH^yf?qG3ZJ
zsO(3b&vOqu`YDP={bR`NJ7!+j>t}mkopp_lC1Okv-7!aruPrEJSe&8J+j;zWvkB=&
z5uYAm2LIK>VCA>Y8H~pRnkB}5da)xsr6yOi=`BCLbX&Q^=ZRkBP*h6nlx9j2z@`a(
zO-ZLPg{4Q06}_)rIroTRi;-IZ?_LnorzFaFU&ho41@+FBRMzm@MXSnt7FPt_|9Fckb3LD-zT0ie(TIp0xQc$qx3k5E
z0g2`9TDe3{v;$B%jj2M(3IDu^!reCCjVEo8`-vz%cg^ceI!5<0YWc(t8>y-{hSr;m
zXz4|d^!6@lw%Vf|(;Tj^({V2`Q+f^dB}W%NxT<5d()4b~zd;IxPpedK??E@rVRm6M
z#5cUc;u2@0x@ZC<@I>~lYd8Zmz56kkVE$ReG(P$Fj3L;^%DiW8p_In!&LBJ?0o$7b##8MfD+`f9y|#Oi)`kW%*
zjPqGy>aW7WYy@sgu(?z<5%WHYeyIn9W%XYRi6A4do}qiUl2>iq4KCX_KIzC^%?n@2
zZ7N#$ba_i=_0;$3yVg!AT1%z$j4xReOxs8lYnv{&ctesBn&(Qhge!Q)wwL&jan%TL
zveU5X@Mq}dF>|VSS@_K-Gk9DH6mOQq1Sri}S;6gciJckJ3U38$^wa0F8Zom#D6PX;)7*nVLltAK9aK%nT-%27<}Oe;>6dC=ggMIVV`h
z_WRgH$ou3JU=;~61m&;tzBd~7cXb_;0Mz$wq1J#UIE{ac0fe7AqC@^h);Hw8dPTEJ
zHVi*vG5DR3?5{Eu9!rUm^PY44pO^YftaXtIJmrn|M!2~fEqi0tJY8>lP~d~$D*%8w
zIxs$dK0wq6RQ@qRm8lg}nTpPpe}(eB#54@iyrxNM(hK8)F(95}1U}8n$py@N0mU7J^v}l+2!oHZ`u0Ld{{k=o#(&P$
zRerJ)3;^A4ogn|C6W~XI$!kt72qlUCA9;7SuHt>#kcT%)r1YwoXZtcnmj+f$GVw5{
zcvZ5W-Vwi?YBP0<)Uc&6fiSrWRHIMs$9zuUqfUkkR>IAdPu4*yi0!)WQUKD-2E`pRdqLvwu##+Wao;@ox1yfn#MZw6(9)6GsZGhN0JJ&^Xv$*9P#XpCGs-J
zWjP*gb6|Y6N-vPaE4JnUd470uIeg=;j!0n=W~OVUqieZTo%567IRfIZ=E_*nU$<*I
zhC5~)S+<+xg1h0alBtgppw)IRY`B`WFNFHLOfhm?vNy1H2ze}Ldzd)8ci4#;RkZ`q
ztOvzkAFs!YjA^5HC)UnuW$uX$Muzmr+&x#3qCget{$F_WkDQDQ!O$)t6#t775C*`2
zsAqtJ(XijO6J!AU;^aoe!p^0pS7hT(8ezQd99}8Bp3(LUvFV-87o@7(nl0j$t={*R
znZpK!R I@FHS^x*>4;@DDIrr
zBIs}k5ES^6blC9xI*<;lpZDjd=*=~ri!0qRQ8Ddqos#7H;5i~>WAxz5L4L=Q
zrl15;xo1%vMe(o@Rcpza92~I#+!`Ga3U#lY95vFeO^^{=2
zYwTn_cDsBjl>=j9brnkC4zI{h+q@k~9m|T#-NjZ_m1>9Yy{!>x9^0V#T$5QR&-LM|
zx<8$*OS3oD5b~_CSv~ui$dZoHNC`O;^VR*0LGlwVhck3+GS^!!Sj^_lNgcF$R7%rfdTkV&D0$ACWgB8b*pcu;
z^*+7(XwrETAZk=fe=+FwasbaE8E1gMy0_vS-%LP!yZ#SRlD|c!i_rDAf$g7&l3@Hl
zkCOZ|z+Z41&*d8b>kpd~YMhe_6__9a)+1w#a&ylqcnCCz`vBc{O%x{`S|;;XI8Iz5
zaf>+Yezj}qqecHHW!5={y<~i2)am|lD8nTIm`!gD(G%x3E%jTpI2geNWQ~OeeWdO&
z5T;A-9@|MPd!M*@kc^%cYesV2DGuwEH{CR=DbkF$PUlLWjVxI@jc%}Hz3=#_G0pG2
z3zpwjL4&8}E2G0gf=ZK*XY6_pwud8Ky{snQZo6eOA2+iFE0iU(Q5Un&v9{FCTwXs&
zukcL{$~Fiu#!~G1&=vznOLHr4>u71QcOCzQI1sa~CZqTMeU^I7Y}J?E*6}-T
zsUNQ(>*NauB~q*qpS{#BJRa`5Y0wuai3E4(v!BwcQ>(2`9<%``rHxum^wGjd`inc*m1PF
zlVp0rITo<1l;+7r$Hv7v=}dZ;jxtK9E@5bSmpu7;x68J0Qxeh#1~N^y6EhpKOzT(E
z`pse-;m?!coSTnjYsH31lVH`iF!8Y(gT{%!Hs$p1T^nzJ9tY3xeSkjLM(QC%+saEWkK;b3GUxI=s+=(VUz463=<9LP2Oc-?;&CpGoD}jZkgBZ^I{R&1&VnD?5{Ek?7&nO-qz2{!pn$()dkfAWTh**e>E2|$}p8tEs6$`)w
zvP94!qyF6&0<-nO)Obq{Wt0Aze!h+
z>|lr(h*?5^O}zRe_VKrPbP=!=*R#s^AN2&oxV5A5^BL3joI3v$903!ze9!$D(Eo7)
z9DdUKkw3o*^nQeY#{2m`8C4Y!G5l}7mj|Hun_njdfKhxn7{w14IB)5H_3QrHj3YpZ
zXodlb1%SB^Xi)I&4?5+x}VhYdppq8v58$E2ghTfP`l6jwNBkX1&
zRXx*}`&4#%Oq)3o-cF5FB>Ti_cX+vLMYi_O0$+Z~Jlu7gVQJ7o>Ad>>^2{-}LEq8-
zVF_?2C93k~P+z~i!ZUfs^(StbD%Ht7X`1P#
zxL+`!E<-7(WN)YUye`w=;YQA|Nh#tyIoAjCi!
z1`ZaTHa(c$_7BNivy~Nc4vw7APbuRkE7zb(8N%^lzX)vKCG>!5T%xb^U)DNE<OmQJcqSH@%p&Pfr>DxE7P%Xz}d!Aw1l#H||wz*qqSmQzq^ftT(<7^Dn`L
z%h7Q
zsCAbO$%lGhmfpg%Un;K?uNsfyMH%k4^nzDK((*o(+Z3HSfXDWbzMf`itTe8jXH>f$
zyg9OFTemug7}ZhnRc)$Xd?k$BBvHN(X3nbqntX^ybh|#II$<9*D;|eI{lNXJ`K~@}WzqIaNpc
qr1;qCx;&1Xa=~b3?a3AUB#F|*d-uL1;e8Q|I<%LOS^vK3=6?Vr#xKGE
diff --git a/desktop/python-embed/_bz2.pyd b/desktop/python-embed/_bz2.pyd
deleted file mode 100644
index 1ea30685c07965f01db931c7a27d593bf0828715..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 84760
zcmd>neS8$v_4jUe0|5dPG|{Lii(PCqsL{~=f{VJ7%!XNiXplm&4(3<7XHS$67q;pP&Ez?mvZ2hhxO(bt4=v4%sqh
zty9@DX6npYb3L_l?zv;mEq8fBx7>aAJz>wSw|nN)-R+rmw@12Sis!C-ZoB=w;lqb|
zt*q}^c<7^MD+
zGd;tyMx5n{0dtdyE&M&6dHlT*>@eT69F9R8)&F!9
zZl^;=UU58ID9;k7W91M$Jx<5C!FY~xIttIn)9-XNlt?DZf1Vl5
z(Mfg4GkM}b-BSPgQObF>W<*{`)YXxL7&%A9^Pe|o?wk;UI8VSE$4~&2Z^}PEs5x)W
z?X&MeMB)+g%0YEL^{9B|6#W0wKmU}gueny7DqbyKBVH>vecA~=%Pjk23ZoLm27L!_R|X34VV3s`0Cl^~Od@JksAIs|jb_-vXt3B+U_s?q7JO$Tkb3
z{SIMt1The2jeCM@v>K5JBom-w$$P4Dly4FbPM*16tJn;!(
zq`VyH)qtM>90R->@HW6Pz2oYa#pdewiBN)o7(*16kwIQjDltDdrvB=zug=e>E&LDCBy&&5d
z&;wb);*C{$RhF#Aa)RuAS(Cg)!Q%I27RHlpL}-xhN8r$4@qP*{sbU|?tifAi238e+
zjKEx3ZO@|6GFjc|R*<&LN~>-dgzypKI_iiN!;b)kO~^};7kxw?5C0K+{
zOlq%N(6xshjsV*kVC_|GvmE_;;pDY6Z1dr{Z+xy``AgA%<<>;D7Jx2(3i$+Wibp?&q?J-HqU#9rpZ0Hi%SFaiPx7^q$hc>b;R<*gHKG`|JI={Fjp?|ngU!21B1&N+6ba|gpoAsgcvjW+E?A^v9nhvE_NXQTao{<
z!j16f$!1H2w6NC$;o_nc`W5t+=@2~EM*-i*!ZMof#aHpi5#1w1mwpS1N#bT98qM1i
z*$S?K1li{3j_~wgsNHCZkV?W<@CU?XDU=qec~lu`EPKSJZ}@bqPeukZYvDSUT>3`*
zxd#9@Z6GB~yji?SyiuHf^G!G2(B4nKhz1I^tLu5bj&M)%7w83vyHUqC_&4%*zwU*lnFbG00^yGjccmUc;)1FJQ~>y_D;0gV-blj2_?9F8jO&Q7zv6tKk~
zRF8xN^_gFy_Ze@>IuczT8n+tr*lLa&0R;~$OIrVoup4#$BHsRG;g>F%qFJB7&aK!WvfN)5Z>>86X;jf@)97z&
zxt6cWMhwarVl8|(`J-spq8#9XOLkB~?~LW;EdNoSHhfXEtNui}qYX`CD6V&1`&c(6
z`@7`ZvZxd#P=+k`QDX}-M-IsR(V($8F(1)^f7MXDo^mbsQQ%(<8e5YuNzko40pB5^
zwJYo!6cXwm6V)9J0GQk&gUEomTmMGMJ7q7Xz%T9FkfwNz_)mD*Eg*kN=a
z)bZKGU5FG6ZRfIrVWCql?pAd7gP>hU?;(BFq`#cGvp_xgkwrBiI{%fJLP9UApXMlR
zN5Iz_o+E41y?Nk)jKxWzSR9k7OUv3sZ&hfQ+Ur_$he*0yXj{;?v*ALK?WXpYL*stV
zzLNEFr^qJ$QS|LzG*n{lrV8K0`|mr;*sRTNIxHJ#xSb2$BF)z$Lw_ukF*fAMY@%0I
zH{`UV7d0I=HlYng-xmv8lU*q0Ff{QhUweZtvh9;Yqng;hAm|p^_(w$F_C-V3#3s=<
ze%*a18r|CTrkJip9#E9_o*9gRJbc1wfY)Y?d|Wxs*x#}K0TQ*?+kEqV4S
zp*tG_`T}PlYSiZk^hr)yR>(Sg0{u7shFB^hXLK`>PX%(Q6)b*0)cr;@CN$F`TwW=x$uEJpY7f=pm^7mY5gS}Mh_TcIRSt!GHu7(*q>jE$mtFiU9JhL~uEBM%q1
zZSR#Z->a=z$v2V7qMK04RPPvB#~)!0%4wFoV*(eog-_=5lWeqM1=fa0Cp2dPUH2J{
zcOI#Es@GGz4cYT5KwM^2XzrVWM!OtpGa~9!C_8ZlxCq*auDWqECTZTAOrj9Wq-#MA
z*u{HzDs9O(5jP)kDU;+5VNFFtv(u<(NYv7J3@*9a5_|B9n$HMm6D!dP%Z2mb82sHXC3NZry4uc)qxJBxGMY@+Z$%By$#Ll>lCi&G`iMabM@6%
zA4pIu)mE{s68n)dC5#HS@qb^6dE#4_pnm{S
zRvWyvFkNs>6qHm4gJB*sJ
zM`l;dGzz>(N5e~r%r?=0gLU%;&&};`SY^$|xPt-IJvtaHOiNU8d<>{>|Iq#~I#
z##=cRS9mzpmlHr#26*vT_}3Dbv`M+LzO0eI<%nyE`LcGcEb5i8He6d)x*^m$J
z6&eLCe?p(@^|cF;6$oPQ$ZSh|6za!z5wT^g-4Yh`B1=-Na=A!C7#NmuySYR(BLkzL
z^f_W=#Z1oesl2$*G}z9vPeNS~Aj|q?Fq&n5{xe>jR#6>q#B|L%;}Z(FNJr17L|jBo
z(LAc2eda{<(-K*~cXsK1iCrQsftX_c`5F_-!YbB8EyjA;Gt?}un8bcV?Qr0&*3U-|
z>(w~6^jqx}iY2Otox-w@AOPws)ZAHu{+>=GY>tl&W0WDEUhS=L~^uNuR1Y+@IAt;z|p0E_!
zFFMQx#CxvD?x29RVjtWF`1R{JD6tNrZZ$BHK52^?|FOAU*$mfX403@l}8Wk^MsC51hP{`^YIpfVg%}AN&Ij
zw{e!uI_`sLRR8h3?f$ie*$I?QhQLbLqMbYs?{0`VDU_zDTk(nm#8X_
za9MS;d#F(nZsq*XQPl|0y%V0L$0BfVy0?bJcP90W~Wc#)Ev=-a!NRH-9844~u-z?#2jF#VYdxrCmXG5)IZH
zvWWrKV0|CrTK&%&tf8axekk75ZWQ$J;45aDS`XW06#SZFHX8*2yd)nZn)uv{S)`Uv
zBDJusV>E}wT-N2arF&)wi~LWQ?vV5fe|!NJhg5TDE*_9#6zqM5NUaHK9JI
zuG=5^T8Ip$m@mVagBi}h1ib`f7zu$jcu7k-l9$M#ih5r~c)`VJ6sX$UWL8lvu68bQ
zXF<7SE~BE}kih_#Z1@rEGW=M+q6n`Lxp4^P>jUXG7%KX{(Tr)o$ABH8Uq0)eYELQ*1(`#w&v6J5
z0%S(~4KR7Su%=LC{u(T4>SkSx&pD281y(4UzXtJTgHjnV@3I_2%*9`IUM_NwQ7Mth
zwc-SMcEcacRua~rnfeqFvFRX9#dDFeGmiP%A$It=IwNY+rYE0Ld-451WXp-GtQ-9k
z=F&u=T-w?CkDO9i-ZUOk(Y)T-h_u0wJUhtzeyKUvDH{ISlt7c?#&TP-OOC<
zh#HS4GI4tOrbrIa<_v~{HR?gv!n?~iNkbZ(K_>cvnk_ZwQ_yT(bc&h?@rV7ufg2>^{&%m
z>Ry)`J3@_hL%Rt15F)D|J{0AlmjKr@w8Zk6`Hn(pu6N{#>p`W#CKZB{*|3z={R~=Dog4F#UtLs!j`}U;0;6CV3KO9d
z>Q`W3+ZC5Fj({bs$0+#tNig=!_yfcE3^7GijjX!W>JfL{v+P=wjFo!)CwGG|%9B_LI!yhKk6Vxz_O93(
zFEqaZvxD(f0tjj6U~3#yZskhVFH^A?G)-(Hun0C|18H`5>g9>s5ybnr|LQ6B_i9;(
z@$gf_SlgHnlSBm?NiN*Vc
zGRB`nj`iP185z
z)*opaSBTpM_?k>C;kqV?f+gL3|&;EUj=j7Y_BBpg`dR!$rUCT-x|EsXkLkR
zOR%m^mDppi5d{mzfO~RiYDD5xuoh0&gqDXWP-yuE#qtOWQTUin*oG5=q1FKF47g$%
zN4GD0g_i=fvDq%To9G8cN+^tyQ92cS#eZ2Wea883ibcQiPS$f@A~t}h!-+g&6X*!4
z9qvlj@+U&zGuB0&+Ka=f9YxkvuD$pt{wh><5{;Nw#iqSvwWBHhLb2&XdN1#YTuiYD
z9dLC9(ajBU7|hm@iz!bzwCOwBkX#X{4Z{&Xwb!y+AV3Pc%@)uAdrx6gy+ut&+-Ph;
z&qH$tF6@G3Kgiyy@_oAKYMNh5yU-3qM1vR1fXLAMjDGo&SHh@gVxzHnUx>}h35I%b
zq_Ax6{irGIL`GZC_qT;>0};$FcL1m0yC2E8%yjC`^p&LfE4#@~sO$TFL`#!p%MdSG+K
zO8VYUaDN%K
zif@Z>{~KV2%os&g@HNG?HOT%Je{?>O&Sweff60l0&=xc+PM}nlRR?QjysH#PDw|tV
zcL|O9_e#xkYoUUMeZow4!3?&zre3tw*C8QjX3Wfqw
z&*#E!mHRjf;bw)e!$W2DJWTWFQR_{ZJG1Wb#EtZ-&CT0XQR_@Dr8i8>n#@MXY&+C7
z(lMeVsD9BMb>~=>GzyA%o`V$dgJ#kk&H!EySyLN9k7Cv;h#c6Iy!cX>EU;Lp2OW8d
zvFHR!@fUD}QC$BCvbT+b>);Q=Gn(B5ZWqJYF-UgxD%uN_m8{LrOFWK7J%(;z`bYKY
z2D(_(w@mAl**>lm#D$r#LRDfqhgthsWGcQ!T~THa5c7h*jhI9?ViK(^l+WfUqhJfs
zq+bp`?uw7hCh|8+tk)
z6J~phgs6auv1_dPeaQ@7-SF7&oZoM3-&9%SOoD;X3D6E+^$GJ98HA6Ahm@*D^tlu5YQd9^qWO!(1ZQ7zGMqk}wTw#Do|%!WTh`p@1AP3YH+Mt+btnP;^7A
z(e{3Sv3t@%6%P~xLw%VnAkLnCtytdlHG
zmW&H{-GxX9mAc0Zvd&I`j@dFHjVHh}}
zxelMt13oqbhQeeYhnbO^H_`UCc@xEx4cxYsr0DFt{xB>W1=ka0mO}mrbQ)QImFguk
zfe$ZF5K4WL_ZryU)lfa=Dl!lv4toFp$okxwGCPk(D5Kz#m4w5{^EIjF(ORDN66oDa
zc*d9I#i>cL0v?M8#3?$5@Fws2EE>?GZN`B=>Ze7zI7ICoE-Zc*krh3=X%qOrQe-O{
zUz`B>aBVaG0qc&sp{N}U4)fX!-}o!a7kr)yinz5t`Tp|HUUoRHTI+C(!_R}?Lr6})
ze7rmHyA;2rfHQx5uHt%nDH2?}Oo+lkmOR{(p%h`Q8>H*CSV9*>wC3%<7;o
zPcWK%Bl=>3Dgy_{nh31B!kW(3c#i1X9L|^6ZX)I{;2`Kb1N0>yLPp_zuhHbe6OuVC
zEg^;T)B;i?RUAGWiL=4Y*D&XrY4*?y3PWf&LQx=!MVXw&BN(1hcwMVFYb%lHYZX*j
zVv!&}BCOdbnuOzo`J3urg`tF+%y>nSj5P(*E-
z=fW6`Ol35hqZfz0a=&heIZ|ZtBwOuF?lHuoOCLd|
zvi71kj-vG|vK4KWw+FAZvhKxGID5T!Ii6xuZzl>9TD|}u08B$Bg8tJn=#2<=Sr-pNm!4RIKEm01ZYr+`}K1eESxhD7kKd
z(@3jZgdd{OF8TI0#G%v4BAM#Xt@8anTomvn7XE;>jsJ=CGTJ`G;LD>bC*;QA22MOxk$P}&@>!n(d$vL;F;bUH2R5~5y
zp=-6Py%z}49|MAypD4VXTNw~uzOC{s{GDBS68<(+!gko=ok@))ysT8>qd*H+j5*{c
zsK^c*M3BdZ@Qta(TWCW=3Cgn}%+)R492*)zki&-ZsCzw)h9g^mpMrV;@Z#W<-{S2t
zq1n3*UCtU*EZIYj=
z)`^Pps!`wflSHBKm+|L|-V!`{)N+a%v!lw0T11Tqq4etm^TkW
z)C+R>bA{JoN3Eo&tL>-?DC{KwLqQLow7DjZCno?yA)3LCN|o7b2Ti7+C+r{|TOriS
zG}PsUqK?hU;<2g;)trX9icpPdsHudiPeWZpsM<8tG(y#+p{^&?v^3OoLRF`sZX%SN
zhN>Zy-$tPkCFxcgAIpHkt{1s=F9bc}eFCjXTQE#$B_NqaxGdSYoLM7R_S_{{kl5D1hBO>j1H5Sg1f}3XHsD5_5Y73W>f|D&AvBr+)w{UqW
zxH1bzT(#qsSh&IzT#<#N2C?ILESx6=S7_m=E$w)D7Ou#`k(2I9ypxme(-a2Q=io`6
zmKN_FR+9RZY+p)niHQ<4%T&ZD#|3`^t`^j<4!V+w8;aI_kHjQ4rF+-+9O
znJF=8p2l1Sv*FV=)&*Nn#2d3BH`{37uu^i6qt%Oi@Zj0HzNW~0eFvZAReT&V1K@ZR
z@jh+kvOFc1Cx8>le1}=fLRXpDtmGjbWW-!eJOaO}Tt=|3IBpQ6S
zAuUP%E_z}*tgk{iSk_5-!pCnf=kf|Z49Tu81wp$A<=UYjbS+|4xnc_D>utbRxw_2K
zc98xtt&*)OTH8hWU|)vhTGdGap4;eI=~@-Do*WO@#_zJ?+0R}J?y;V6D;kx&+hiWEa`cNuePjNB`N!3n;iC3xc6B#gZJ~f=0C>5r*Ood|^FbPI#__hpq
zR|dQ{1CD3FJsEIs227nIwLI!GsWA1kRG9i=Dop(~6{bF&3YTQSG&ZD$`!irU1Fp`1
zr)9u38Su;uxHbc>&wv{<;N}c?c?P^P1AZb6Mt30DokqUm*qhT`g}BRN-pbXgzr-U>pCwnX1Kperv)1v7Y*
z10hLwG&4?#Ns8rBNYXlwLXwtx6q2j3oY<5+xqh38Bz^d(Liz_n$^#**2SQF82w5`_a^^tD+JTVu10fp+LN*VCTs{zTOGm)6Zlo?R_Y9V>>iJ{jyc%{jW-oqxEbkar)$J{6(!GITr?mLr&72g$V
zxV3~ds@fS?B-T@seaLBr_h)#86^b(YDPGqZU61%g?&|>?6SioS3oo~D`vbIh9~|_m
z+Jq|Xcx_d%Ey{@DYpicm!yareSy7GDlIuf*Z^G}EnopLE4w`#wxv|5vS|l)#^;<3W
z(6BbEi)=i>&`zw{+}Fr1oYW8{3k)sduF1-Pp#Z0a<9loxoRa|$&46<=;1L;cUIu(Z
z2ArP(pOgU?X27Rrz@swYGcsUL27G1)3rz8L*rIPtJg0N=@V6|X&|uwo;~=J@Bk@CC7{gIh37D|b-+)LHj;8W`ZuVH!w9V*~ZuYHXu$3oTpr;akyE
zFHRo0suEV}Ww+0yr7J#U!={KHZ*Sidalpt|F!y4hc~{Z(Lki-qS;3=_mruVxPe54n
z^XIUWJit1ni*|%hhxd)JrW>w5i0;4&Nc4BvVvzbp&O*-phBEwg2UcoB%
zNr3%bT<5`{hp~!0^d`xCJ3p0ZpRXdU!3Ty`U#CP)zL$4m&$`fZ^$-j^@EcO&&bkk<
z{}0|oop632%eT<-0^a1%)&MLKv$pdO0kgVHhPBaW1k)?f^%m5!Y4pj}wI!}?g*mz-
zXl$^)94Nkj2`#q(2ddz0Sw)^&y?KKAzf^3fdu<*F#<4Yx@Ems2m`<5APQb^#iG%S@
z`zb-!XBcDEw!Ef84h)YCud+Z`)*s_j07h!!)9aMYjOC3!RKCO7F|b7x9*V_h{R^!O
zLE%Q~4|s8k4)Yrl(bB#~(f`y)X$9I}FE&o}qG%xpgRrq7sBX-o;(5Hfe_F!DZVU_h
zUv4M)!k3A4M6$&03d`79^zFwG4_Ld`!QT={m{GRG6To554mKG)PI4fzPe3Hv?Wd>M
zWWwz;-i}zp8kf=G>uvZ1-<6P3)T^EGXMxA}2rif%ebRj+Fa7o~Xg6EoDb57g>FKj&
zO@Muk?|e5p)@5@tVE5S)zZDq;V8r(a*|$~fU&81{gh46?`*sN}A0r}m>)>_SBQVIP
zlwZuthfg9p2DaN_?hH0<~A
z9hv<}F7cq*t{k%P##6nD$&kwHRqETXFiVcH*s7diAUDr9{q
zl4tdjIKm$5z6l?Rh#UAogq-qA(6vpO{I(JY^u%xZ2n@h9dQ%osmm|U;j-G6zai~uU
zy3|GwN<)vb(QtZ6iJfhu;enBY{_LMtk-2H;cWgBLw^CxqY&6`NQqa%X=o8Y=zp&A8
z1WAcK-$rA*O$z!p8(o-&uCmeiMxPS<$2NLY8hW&iJ|hjCW1~H3=r8_ZRsPI0^t(11
z+fGu7Y`4+yN>4$rve89p=!a}Hyxme_FSOC)($II<==0Lh)i$~$4L!j|UzmnI(?*X^
zLl3jj@B>ewA=M
z#70YL=qWb3G7UY^M$2jFu{L^g8hV6{R?^T1_F9x&o`&9QqcOLp)UwM)UzLV_-bPPN
zLqBSxuK}8ObKZ+1HX62q6!cv-`ua5VH8y&B8v0TjeUmwUuq{0AA{$+k7JH8vDSV$|F^-_A32~!}wZug`Ruefn5Q86wYpGL@dV)
zQJdwi(u2LoPbO9#ZatI(zp7!~I7XxOV2RkRFO|%;4}*tZ8&89Z{vA0D
z3WmDMo81?Hl5tl6u5^|+OBXl+&b}gx;4Ff(IP50aMX-y*L`kKaU^j<};>v7-vpGyP
zs~kk|AP(meJec6Y93DY%4#7Db&LemT!9zGqn}jQe5IIA~=`eTn-l!
zJe=U+96puc5d@Fm@F;=>f&~trL2w?yc^vi-Jd)s%96poa69_(m!(M_Km4~P8(
zk0E#rhs(`Aay@Cy6*)ME(hs5TUq52EDUQYQ_lizGQ65
z(;)mdR+@uEc4&r3o1oM~=teK0f53U}2$bSoRr=*e6xW-}QTmnWCc(qP%Wc8dWX=r4
z7GCZQ4%>`lpo~rpJ3d#6+Qf+Zxf>aZ*_aPvRvdX)cVC=Pr$|QvY3>%v(2dOcGV1J$
zv@)ROM<^FaGK!fY4QnG(a9Gi!MrXO^eiSDfAsa(f_u=d%cZ+Jkr|JeQBypt91Ud9h
z(DgQ!vpAW_n1Oy}QUaGk;pJ9jg+cx07*3xuy6AkUP^(F_bKVxL9N5Ob`9Qvy7L)=G
z6QwT=ky@kG-@M9AzJYKsYC33uMGLV{Y?Zee!t<92Gi31?Q*0+<;KZXwl4I=xv-{K^fTNJ4TEZwIb`q*acGoxIm{kVXa+^y+$nz
zhk!Vw;#`K^H?paGX9Y*q;mD-$CHU~26Lf)siK?uRS4CmnCk1mx
zOYCk8KI;w?;Lm>w;OZ9eeJ(6}4%L>(HL?j_w;ot7TaAryRc-o~Q?gpoU-S|;6<3eg
z2oLaR=`^rb9~T8b;6SeE?&2WZ)=v+ewDk+BqOh)~K#Szo;+5U+hRZG?Ab>z&ZBCAQ
za?`=XnD`dHp2<$sl|1MU9?#EheZ&&`alOH6aHN9?g`HUaRy*2d4;DiVumDn$rE^
zra{hN6P*X{SWqFc5fXc!{i?4dVcnMnV)6E-LnIZ#vMtxZb@FF8sj&kao3s%q9%uIM
zG?1JJZ$NO+=upT<&dO6*8h}EALxpLY#SFR%QAlSb86~q7O~Rxj{_p?b*|dRXX>&6w
zKLidQKw@!+of^||wV;1-HKkndb)XTkAA?U}3RyQ~%xKeSnO{an78jBCo+thX(8${y
zzLJt_wzpcn6ow$kfY4G0swuUS_!JK5qJB5@rp;pU5xMwHCbiU-YtmylA#JzsTVZJh
zBG6v4;!Vd8?+3;pM)Gyy=}an4!ZaClbyn&5Eq(nr7&-yl62UNW=RutAlzXju=uDx7
zzTsjVj_~QW
z(BRB0HR*)x<6kH#yX@f_pm>-MnEcF94Q@
ziDixJC-VI3PLe|{>q$iUe)#+rEy5vqM+en*6lV%z!>eYQ7op5@-F*%YVdIreoD6o;
zS#z#!2|r%rZ~Zt!U!y$>h3Q9BS3@m?(pLUEJWQp{4-A$htMPz_1}$zm@q+CJZY^-H
zqq!ZxdeDbxoPaA`gln-KNP`{-bl-R;!}+ffFo{g9tQxlpY8BPn*edJRC;w3XT3L6m
zC7W6`At_rWG_el}zg9C}Hogy60~@PY+(tT}pJY%{d-E6EMeaqNAQ0QrXl%n$do!$G
zB@_XYieoG-e`aHdF
zy0Kk7GGftEoQim|f}HCyOb20l<)e__n<*yG)td&FgB^Pa%gvvId
za=UoZc40AnO@mi^fV~@Fp9fi675)E-Js;2-Zkfs%N^C?o!%(m1UO?GiP`t%HQL&w$
zsMuC%o_&ZfPe`|LXA)b-=tRXf%w5TsfLb&W>0eDrA0C8L6@Qo|8m&%=|jj^NaEP{E~i+Gf@1k
zq`S|@h#8+nR8FWm
z#I}(9iN#w%fv
z$Aylj!@8%5S}{FqL=7(c5iiw2doe=2N?RX!5f53f&yqCrxOPcviI|=6smKb%l(bb5
zFT^P^8smb<+Ht;*#9|oUSpsMFf$zy>ybu{9c{NeS>qkc1TDx3HE7L2XsJRk*FvkWi
zYK6WQ!`>2M(ze7k$W>7o-)g|%Uaw>O5@0DC7;yc5UT
zLj<8>;v_h-UhuQ=^wY1po4VI#ShhpkXh*&glO~L8z-e