diff --git a/frontend/app.js b/frontend/app.js index 9916e51..0734f78 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -18,6 +18,10 @@ let editorVault = null; let editorPath = null; let fallbackEditorEl = null; + const panelState = { + vault: true, + tag: true, + }; // --------------------------------------------------------------------------- // File extension → Lucide icon mapping @@ -148,6 +152,14 @@ }); } + 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"); + } + // --------------------------------------------------------------------------- // API helpers // --------------------------------------------------------------------------- @@ -188,14 +200,36 @@ // --------------------------------------------------------------------------- function initVaultContext() { const filter = document.getElementById("vault-filter"); + const quickSelect = document.getElementById("vault-quick-select"); + if (!filter || !quickSelect) return; + filter.addEventListener("change", async () => { - selectedContextVault = filter.value; - showingSource = false; - cachedRawSource = null; - await refreshSidebarForContext(); - await refreshTagsForContext(); - showWelcome(); + 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(); + 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"); + if (filter) filter.value = selectedContextVault; + if (quickSelect) quickSelect.value = selectedContextVault; } async function refreshSidebarForContext() { @@ -223,6 +257,19 @@ safeCreateIcons(); } + async function focusVaultInSidebar(vaultName) { + setPanelExpanded("vault", true); + 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); + } + vaultItem.scrollIntoView({ block: "nearest" }); + } + async function refreshTagsForContext() { const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`; const data = await api(`/api/tags${vaultParam}`); @@ -237,7 +284,10 @@ allVaults = vaults; const container = document.getElementById("vault-tree"); const filter = document.getElementById("vault-filter"); + const quickSelect = document.getElementById("vault-quick-select"); container.innerHTML = ""; + filter.innerHTML = ''; + quickSelect.innerHTML = ''; vaults.forEach((v) => { // Sidebar tree entry @@ -258,16 +308,24 @@ opt.value = v.name; opt.textContent = v.name; filter.appendChild(opt); + + const quickOpt = document.createElement("option"); + quickOpt.value = v.name; + quickOpt.textContent = v.name; + quickSelect.appendChild(quickOpt); }); + syncVaultSelectors(); safeCreateIcons(); } - async function toggleVault(itemEl, vaultName) { + async function toggleVault(itemEl, vaultName, forceExpand) { const childContainer = document.getElementById(`vault-children-${vaultName}`); if (!childContainer) return; - if (childContainer.classList.contains("collapsed")) { + const shouldExpand = forceExpand || childContainer.classList.contains("collapsed"); + + if (shouldExpand) { // Expand — load children if empty if (childContainer.children.length === 0) { await loadDirectory(vaultName, "", childContainer); @@ -668,6 +726,76 @@ area.scrollTop = 0; } + // --------------------------------------------------------------------------- + // Collapsible panels and help modal + // --------------------------------------------------------------------------- + function initCollapsiblePanels() { + bindPanelToggle("vault", "vault-panel-toggle", "vault-panel-content"); + bindPanelToggle("tag", "tag-panel-toggle", "tag-panel-content"); + setPanelExpanded("vault", true); + setPanelExpanded("tag", true); + } + + function bindPanelToggle(panelKey, toggleId, contentId) { + const toggle = document.getElementById(toggleId); + const content = document.getElementById(contentId); + if (!toggle || !content) return; + toggle.addEventListener("click", () => { + setPanelExpanded(panelKey, !panelState[panelKey]); + }); + } + + function setPanelExpanded(panelKey, expanded) { + panelState[panelKey] = expanded; + const toggle = document.getElementById(`${panelKey}-panel-toggle`); + const content = document.getElementById(`${panelKey}-panel-content`); + if (!toggle || !content) return; + toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); + content.classList.toggle("collapsed", !expanded); + const iconEl = toggle.querySelector("[data-lucide]"); + if (iconEl) { + iconEl.setAttribute("data-lucide", expanded ? "chevron-down" : "chevron-right"); + } + if (panelKey === "tag") { + const tagSection = document.getElementById("tag-cloud-section"); + const resizeHandle = document.getElementById("tag-resize-handle"); + if (tagSection) tagSection.classList.toggle("collapsed-panel", !expanded); + if (resizeHandle) resizeHandle.classList.toggle("hidden", !expanded); + } + safeCreateIcons(); + } + + 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(); + }); + + 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 closeHelpModal() { + const modal = document.getElementById("help-modal"); + if (modal) modal.classList.remove("active"); + } + // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- @@ -1112,6 +1240,8 @@ initSearch(); initMobile(); initVaultContext(); + initCollapsiblePanels(); + initHelpModal(); initSidebarFilter(); initSidebarResize(); initTagResize(); diff --git a/frontend/index.html b/frontend/index.html index 6bc9927..a95b3c3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -102,6 +102,14 @@ Clair + + @@ -122,16 +130,32 @@
-
Tags
-
+ +
@@ -171,6 +195,52 @@ + +
+
+
+
Aide ObsiGate
+
+ +
+
+
+
+

Guide d'utilisation

+

ObsiGate vous permet d'explorer vos notes, de naviguer rapidement entre les vaults et d'utiliser les tags pour filtrer ou retrouver vos contenus.

+ +

Présentation générale de l'interface

+

L'application est organisée autour de trois zones principales : l'en-tête avec la recherche et les options, la barre latérale avec les vaults et les tags, et la zone centrale pour consulter, rechercher et modifier les fichiers.

+ +

Sections principales

+

Vaults

+

La section Vaults regroupe les espaces suivants : Recettes, IT, Main, Workout, Sessions et Bruno. Vous pouvez ouvrir un vault pour explorer son arborescence, puis sélectionner un fichier pour l'afficher.

+

Tags

+

La section Tags affiche les étiquettes disponibles comme #serveur, #TDM, #local, #linux, #debian, #docker, #None, #documentation, #network, #fiche-equipement, #fiche-infra, #fiche-reseau, #ssh et #bruno. Cliquez sur un tag pour lancer un filtrage rapide.

+ +

Éléments interactifs

+

Sélection rapide des vaults

+

Le menu déroulant en haut de l'arborescence vous permet de vous focaliser instantanément sur un vault précis ou de revenir à l'affichage global avec Tous les vaults.

+

Panneaux réductibles

+

Les sections Vaults et Tags peuvent être réduites ou développées depuis leur en-tête pour gagner de l'espace dans la barre latérale.

+ +

Actions courantes

+

Créer ou modifier

+

Ouvrez un fichier puis utilisez l'action d'édition pour modifier son contenu. Le mode source et la sauvegarde sont accessibles depuis la zone de lecture.

+

Rechercher

+

Utilisez la barre de recherche en haut pour retrouver des notes par texte. Vous pouvez combiner la recherche avec un vault sélectionné et un ou plusieurs tags.

+

Naviguer

+

Dépliez les dossiers et vaults dans la barre latérale, ou utilisez directement la sélection rapide pour aller au bon contexte plus vite.

+ +

Astuces

+

Le thème clair/sombre est disponible dans le menu Options. Vous pouvez aussi redimensionner la barre latérale et la zone des tags sur desktop pour adapter l'affichage à votre usage.

+
+
+
+
+ - - + + diff --git a/frontend/style.css b/frontend/style.css index 607a87b..2752eeb 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -367,7 +367,36 @@ a:hover { text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); - padding: 8px 16px 4px; + padding: 0; +} + +.sidebar-panel-toggle { + width: 100%; + border: none; + background: transparent; + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 16px 10px; + cursor: pointer; + transition: color 150ms ease, background 150ms ease; +} + +.sidebar-panel-toggle:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.sidebar-panel-toggle .icon, +.sidebar-panel-toggle [data-lucide] { + color: inherit; + flex-shrink: 0; +} + +.sidebar-panel-content.collapsed { + display: none; } .tree-item { @@ -393,6 +422,11 @@ a:hover { 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.filtered-out { display: none; } @@ -438,13 +472,19 @@ a:hover { /* --- Tag Cloud --- */ .tag-cloud-section { - padding: 12px 16px; + padding: 12px 0 0; height: 180px; min-height: 60px; max-height: 400px; overflow-y: auto; flex-shrink: 0; } +.tag-cloud-section.collapsed-panel { + height: auto; + min-height: 0; + max-height: none; + overflow: hidden; +} .tag-cloud-section::-webkit-scrollbar { width: 6px; } @@ -460,13 +500,14 @@ a:hover { text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); - margin-bottom: 8px; + margin-bottom: 0; } .tag-cloud { display: flex; flex-wrap: wrap; gap: 6px; + padding: 0 16px 12px; } .tag-item { @@ -487,6 +528,10 @@ a:hover { display: none; } +.tag-resize-handle.hidden { + display: none; +} + /* --- Sidebar resize handle (horizontal) --- */ .sidebar-resize-handle { width: 5px; @@ -918,6 +963,9 @@ a:hover { .editor-modal.active { display: flex; } +.help-container { + max-width: 960px; +} .editor-container { background: var(--bg-secondary); border: 1px solid var(--border); @@ -990,6 +1038,46 @@ a:hover { overflow-y: auto; overflow-x: hidden; } +.help-body { + padding: 0; + background: var(--bg-primary); +} +.help-content { + max-width: 820px; + margin: 0 auto; + padding: 28px 24px 40px; +} +.help-content h1, +.help-content h2, +.help-content h3 { + font-family: 'JetBrains Mono', monospace; + color: var(--text-primary); +} +.help-content h1 { + font-size: 1.6rem; + margin-bottom: 16px; +} +.help-content h2 { + font-size: 1.05rem; + margin: 24px 0 10px; +} +.help-content h3 { + font-size: 0.9rem; + margin: 16px 0 6px; + color: var(--text-secondary); +} +.help-content p { + margin: 0 0 12px; + color: var(--text-secondary); +} +.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); +} .cm-editor { height: auto; min-height: 100%; @@ -1149,6 +1237,18 @@ body.resizing-v { min-width: 200px; } + .sidebar-quick-select { + padding: 0 12px 12px; + } + + .sidebar-panel-toggle { + padding: 8px 12px 10px; + } + + .tag-cloud { + padding: 0 12px 12px; + } + .sidebar { position: fixed; top: 0; @@ -1181,6 +1281,10 @@ body.resizing-v { min-height: 80px; } + .help-content { + padding: 20px 16px 28px; + } + .content-area { padding: 16px 12px 60px; }