Add collapsible sidebar panels, vault quick-select dropdown, help modal, and vault focus feature with improved navigation UX
This commit is contained in:
parent
b73aa19c51
commit
a42f61e59a
138
frontend/app.js
138
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;
|
||||
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 = '<option value="all">Tous les vaults</option>';
|
||||
quickSelect.innerHTML = '<option value="all">Tous les vaults</option>';
|
||||
|
||||
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();
|
||||
|
||||
@ -102,6 +102,14 @@
|
||||
<span id="theme-label">Clair</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-section">
|
||||
<div class="menu-label">Aide</div>
|
||||
<button class="menu-theme-btn" id="help-open-btn">
|
||||
<i data-lucide="circle-help" style="width:16px;height:16px"></i>
|
||||
<span>Ouvrir l'aide</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,17 +130,33 @@
|
||||
</div>
|
||||
|
||||
<div class="sidebar-tree" id="sidebar-tree">
|
||||
<div class="sidebar-section-title">Vaults</div>
|
||||
<div class="sidebar-quick-select">
|
||||
<label class="sr-only" for="vault-quick-select">Sélection rapide des vaults</label>
|
||||
<select id="vault-quick-select" class="menu-select sidebar-quick-select-control">
|
||||
<option value="all">Tous les vaults</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="sidebar-panel-toggle" id="vault-panel-toggle" type="button" aria-expanded="true" aria-controls="vault-panel-content">
|
||||
<span class="sidebar-section-title">Vaults</span>
|
||||
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
<div class="sidebar-panel-content" id="vault-panel-content">
|
||||
<div id="vault-tree"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tag resize handle -->
|
||||
<div class="tag-resize-handle" id="tag-resize-handle"></div>
|
||||
|
||||
<div class="tag-cloud-section" id="tag-cloud-section">
|
||||
<div class="tag-cloud-title">Tags</div>
|
||||
<button class="sidebar-panel-toggle" id="tag-panel-toggle" type="button" aria-expanded="true" aria-controls="tag-panel-content">
|
||||
<span class="tag-cloud-title">Tags</span>
|
||||
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
<div class="sidebar-panel-content" id="tag-panel-content">
|
||||
<div class="tag-cloud" id="tag-cloud"></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Sidebar resize handle -->
|
||||
@ -171,6 +195,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Modal -->
|
||||
<div class="editor-modal" id="help-modal">
|
||||
<div class="editor-container help-container">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title" id="help-title">Aide ObsiGate</div>
|
||||
<div class="editor-actions">
|
||||
<button class="editor-btn" id="help-close" title="Fermer l'aide" aria-label="Fermer l'aide">
|
||||
<i data-lucide="x" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-body help-body" id="help-body">
|
||||
<div class="help-content">
|
||||
<h1>Guide d'utilisation</h1>
|
||||
<p>ObsiGate vous permet d'explorer vos notes, de naviguer rapidement entre les vaults et d'utiliser les tags pour filtrer ou retrouver vos contenus.</p>
|
||||
|
||||
<h2>Présentation générale de l'interface</h2>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Sections principales</h2>
|
||||
<h3>Vaults</h3>
|
||||
<p>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.</p>
|
||||
<h3>Tags</h3>
|
||||
<p>La section Tags affiche les étiquettes disponibles comme <code>#serveur</code>, <code>#TDM</code>, <code>#local</code>, <code>#linux</code>, <code>#debian</code>, <code>#docker</code>, <code>#None</code>, <code>#documentation</code>, <code>#network</code>, <code>#fiche-equipement</code>, <code>#fiche-infra</code>, <code>#fiche-reseau</code>, <code>#ssh</code> et <code>#bruno</code>. Cliquez sur un tag pour lancer un filtrage rapide.</p>
|
||||
|
||||
<h2>Éléments interactifs</h2>
|
||||
<h3>Sélection rapide des vaults</h3>
|
||||
<p>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 <code>Tous les vaults</code>.</p>
|
||||
<h3>Panneaux réductibles</h3>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Actions courantes</h2>
|
||||
<h3>Créer ou modifier</h3>
|
||||
<p>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.</p>
|
||||
<h3>Rechercher</h3>
|
||||
<p>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.</p>
|
||||
<h3>Naviguer</h3>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Astuces</h2>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user