Help: mobile slide-over TOC with overlay, search content highlighting
CI / lint (push) Failing after 4s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 7s

This commit is contained in:
2026-06-03 09:27:06 -04:00
parent 99b00eea0c
commit 03586256f1
3 changed files with 163 additions and 83 deletions
+6 -2
View File
@@ -762,6 +762,9 @@
<div class="editor-modal" id="help-modal">
<div class="editor-container help-container">
<div class="editor-header">
<button class="help-hamburger" id="help-hamburger" title="Sommaire" aria-label="Afficher le sommaire">
<i data-lucide="menu" style="width:18px;height:18px"></i>
</button>
<div class="editor-title" id="help-title">Guide d'utilisation ObsiGate</div>
<div class="editor-actions">
<button class="editor-btn" id="help-close" title="Fermer l'aide" aria-label="Fermer l'aide">
@@ -769,12 +772,13 @@
</button>
</div>
</div>
<div class="help-nav-overlay" id="help-nav-overlay"></div>
<div class="editor-body help-body" id="help-body">
<nav class="help-nav" id="help-nav">
<div class="help-nav-header">
<div class="help-nav-title">Sommaire</div>
<button class="help-nav-toggle" id="help-nav-toggle" title="Afficher/Masquer le sommaire" aria-label="Menu sommaire">
<i data-lucide="chevron-down" style="width:16px;height:16px"></i>
<button class="help-nav-close" id="help-nav-close" title="Fermer" aria-label="Fermer le sommaire">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
</div>
<div class="help-nav-search-wrap">
+79 -37
View File
@@ -225,7 +225,9 @@ function initHelpNavigation() {
const navList = document.getElementById("help-nav-list");
const nav = document.getElementById("help-nav");
const searchInput = document.getElementById("help-nav-search");
const toggleBtn = document.getElementById("help-nav-toggle");
const hamburger = document.getElementById("help-hamburger");
const navClose = document.getElementById("help-nav-close");
const overlay = document.getElementById("help-nav-overlay");
const helpContent = document.querySelector(".help-content");
if (!helpBody || !navList || !nav) return;
@@ -235,27 +237,41 @@ function initHelpNavigation() {
const sectionMap = {};
allSections.forEach(s => { sectionMap[s.id] = s; });
// Build text index for content search (section title + text content)
// Build text index for content search
const sectionTextCache = {};
allSections.forEach(s => {
sectionTextCache[s.id] = (s.textContent || '').toLowerCase();
});
// ── Mobile: open/close TOC slide-over ──
function openTOC() {
nav.classList.add("open");
if (overlay) overlay.classList.add("visible");
if (searchInput) setTimeout(() => searchInput.focus(), 150);
document.body.style.overflow = "hidden";
}
function closeTOC() {
nav.classList.remove("open");
if (overlay) overlay.classList.remove("visible");
document.body.style.overflow = "";
}
function isMobile() { return window.innerWidth <= 768; }
if (hamburger) hamburger.addEventListener("click", openTOC);
if (navClose) navClose.addEventListener("click", closeTOC);
if (overlay) overlay.addEventListener("click", closeTOC);
// ── Scroll to section ──
function scrollToSection(targetId) {
const section = sectionMap[targetId];
if (!section) return;
section.scrollIntoView({ behavior: "smooth", block: "start" });
section.setAttribute("tabindex", "-1");
// On mobile, collapse nav after selection
if (window.innerWidth <= 768 && nav) {
nav.classList.add("collapsed");
}
if (isMobile()) closeTOC();
}
// ── Focus the content pane (first heading in active section) ──
// ── Focus the content pane ──
function focusContent() {
// Find the active section and focus its first heading
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
if (activeLink) {
const targetId = activeLink.getAttribute("href").substring(1);
@@ -265,12 +281,12 @@ function initHelpNavigation() {
if (h2) { h2.setAttribute("tabindex", "-1"); h2.focus({ preventScroll: true }); return; }
}
}
// Fallback: focus the content area itself
if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.focus({ preventScroll: true }); }
}
// ── Focus TOC active link ──
function focusTOC() {
if (isMobile() && !nav.classList.contains("open")) openTOC();
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
if (activeLink) { activeLink.focus(); return; }
const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
@@ -301,11 +317,9 @@ function initHelpNavigation() {
const prev = (currentIdx - 1 + items.length) % items.length;
items[prev].focus();
} else if (e.key === "ArrowRight") {
// Right arrow: go to content pane
e.preventDefault();
focusContent();
} else if (e.key === "ArrowLeft") {
// Left arrow in TOC: stay (prev item)
e.preventDefault();
const prev = (currentIdx - 1 + items.length) % items.length;
items[prev].focus();
@@ -319,12 +333,10 @@ function initHelpNavigation() {
e.preventDefault();
const targetId = items[currentIdx].getAttribute("href").substring(1);
scrollToSection(targetId);
// After scrolling, move focus to content
setTimeout(() => focusContent(), 400);
}
});
// Make nav items focusable
navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
// ── Keyboard navigation in content pane ──
@@ -332,7 +344,6 @@ function initHelpNavigation() {
helpContent.setAttribute("tabindex", "-1");
helpContent.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
// Left arrow in content: go back to TOC
e.preventDefault();
focusTOC();
} else if (e.key === "ArrowUp" && document.activeElement === helpContent) {
@@ -353,7 +364,7 @@ function initHelpNavigation() {
});
}
// ── Search: filter TOC + hide non-matching content sections ──
// ── Search: filter TOC + hide non-matching sections + highlight text ──
let currentQuery = "";
if (searchInput) {
searchInput.addEventListener("input", () => {
@@ -369,8 +380,57 @@ function initHelpNavigation() {
});
}
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();
@@ -380,7 +440,7 @@ function initHelpNavigation() {
link.classList.add("hidden");
}
});
// Filter content sections
// Filter content sections + highlight
allSections.forEach((section) => {
if (!q) {
section.classList.remove("help-hidden");
@@ -390,12 +450,13 @@ function initHelpNavigation() {
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();
// If no visible results, show a message
ensureNoResultsMessage(q);
}
@@ -415,26 +476,7 @@ function initHelpNavigation() {
}
}
// ── Mobile toggle ──
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
const wasCollapsed = nav.classList.contains("collapsed");
nav.classList.toggle("collapsed");
// If expanding, focus search
if (wasCollapsed && searchInput) {
setTimeout(() => searchInput.focus(), 150);
}
});
// Start expanded on mobile so user sees the TOC immediately
// (we use collapsed only after user taps a section or explicitly toggles)
window.addEventListener("resize", () => {
if (window.innerWidth > 768) {
nav.classList.remove("collapsed");
}
});
}
// ── Scroll spy (IntersectionObserver) ──
// ── Scroll spy ──
function updateActiveLink() {
const visibleLinks = allLinks.filter(l => !l.classList.contains("hidden"));
const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
+78 -44
View File
@@ -2178,6 +2178,24 @@ select {
}
/* 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-overlay {
display: none;
}
.help-nav {
width: 260px;
min-width: 260px;
@@ -2209,7 +2227,7 @@ select {
letter-spacing: 0.05em;
color: var(--text-muted);
}
.help-nav-toggle {
.help-nav-close {
display: none;
width: 32px; height: 32px;
padding: 0;
@@ -2221,7 +2239,7 @@ select {
align-items: center;
justify-content: center;
}
.help-nav-toggle:hover { color: var(--accent); border-color: var(--accent); }
.help-nav-close:hover { color: var(--accent); border-color: var(--accent); }
.help-nav-search-wrap {
padding: 0 12px 8px;
flex-shrink: 0;
@@ -2290,6 +2308,12 @@ select {
.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;
@@ -6393,61 +6417,71 @@ body.popup-mode .content-area {
/* ── Help Modal: Mobile responsive ── */
@media (max-width: 768px) {
.help-container { max-width: 100%; height: 100vh; }
.help-container .editor-container {
.help-container {
max-width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
}
/* Hamburger visible on mobile */
.help-hamburger {
display: inline-flex;
}
/* TOC: slide-over from left */
.help-nav-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 1100;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.help-nav-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.help-nav {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 280px;
min-width: 280px;
max-height: 100vh;
height: 100vh;
z-index: 1200;
transform: translateX(-100%);
transition: transform 0.25s ease;
border-right: 1px solid var(--border);
box-shadow: 2px 0 16px rgba(0,0,0,0.3);
flex-shrink: 0;
}
.help-nav.open {
transform: translateX(0);
}
.help-nav-close {
display: inline-flex;
}
/* Content full-width */
.help-body {
flex-direction: column;
overflow-y: auto;
}
.help-nav {
width: 100%;
min-width: 100%;
max-height: none;
height: auto;
position: relative;
top: auto;
border-right: none;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
transition: max-height 0.25s ease;
overflow: hidden;
}
.help-nav-header {
padding: 10px 16px 6px;
cursor: pointer;
}
.help-nav-toggle {
display: inline-flex;
transition: transform 0.2s;
}
.help-nav:not(.collapsed) .help-nav-toggle {
transform: rotate(180deg);
}
.help-nav.collapsed .help-nav-search-wrap,
.help-nav.collapsed .help-nav-list {
display: none;
}
.help-nav-search-wrap {
padding: 0 12px 6px;
}
.help-nav-list {
max-height: 45vh;
flex: none;
padding-bottom: 14px;
padding: 0;
}
.help-content {
padding: 14px 12px 40px;
padding: 14px 16px 40px;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.help-content:focus {
outline: none;
}
.help-content:focus { outline: none; }
.help-hero {
flex-direction: column;
gap: 16px;