Add help navigation search, mobile toggle, and scroll spy
This commit is contained in:
+10
-2
@@ -771,8 +771,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="editor-body help-body" id="help-body">
|
<div class="editor-body help-body" id="help-body">
|
||||||
<nav class="help-nav" id="help-nav">
|
<nav class="help-nav" id="help-nav">
|
||||||
<div class="help-nav-title">Sommaire</div>
|
<div class="help-nav-header">
|
||||||
<ul class="help-nav-list">
|
<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="menu" style="width:16px;height:16px"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="help-nav-search-wrap">
|
||||||
|
<input type="text" class="help-nav-search" id="help-nav-search" placeholder="Rechercher dans l'aide..." autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<ul class="help-nav-list" id="help-nav-list">
|
||||||
<li><a href="#help-intro" class="help-nav-link">📘 Introduction</a></li>
|
<li><a href="#help-intro" class="help-nav-link">📘 Introduction</a></li>
|
||||||
<li><a href="#help-interface" class="help-nav-link">🧭 Interface</a></li>
|
<li><a href="#help-interface" class="help-nav-link">🧭 Interface</a></li>
|
||||||
<li><a href="#help-navigation" class="help-nav-link">🗺️ Navigation</a></li>
|
<li><a href="#help-navigation" class="help-nav-link">🗺️ Navigation</a></li>
|
||||||
|
|||||||
+141
-63
@@ -211,57 +211,161 @@ function initHelpModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initHelpNavigation() {
|
function initHelpNavigation() {
|
||||||
const helpContent = document.querySelector(".help-content");
|
|
||||||
const helpBody = document.getElementById("help-body");
|
const helpBody = document.getElementById("help-body");
|
||||||
const navLinks = document.querySelectorAll(".help-nav-link");
|
const navList = document.getElementById("help-nav-list");
|
||||||
const navList = document.querySelector(".help-nav-list");
|
const nav = document.getElementById("help-nav");
|
||||||
|
const searchInput = document.getElementById("help-nav-search");
|
||||||
|
const toggleBtn = document.getElementById("help-nav-toggle");
|
||||||
|
|
||||||
if (!helpContent || !helpBody || !navLinks.length) return;
|
if (!helpBody || !navList || !nav) return;
|
||||||
|
|
||||||
// Handle nav link clicks
|
const allLinks = [...navList.querySelectorAll(".help-nav-link")];
|
||||||
navLinks.forEach((link) => {
|
|
||||||
|
// ── Scroll to section and focus it ──
|
||||||
|
function scrollToSection(targetId) {
|
||||||
|
const section = document.getElementById(targetId);
|
||||||
|
if (!section) return;
|
||||||
|
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
section.setAttribute("tabindex", "-1");
|
||||||
|
section.focus({ preventScroll: true });
|
||||||
|
// On mobile, collapse nav after selection
|
||||||
|
if (window.innerWidth <= 768 && nav) {
|
||||||
|
nav.classList.add("collapsed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nav link click handler ──
|
||||||
|
allLinks.forEach((link) => {
|
||||||
link.addEventListener("click", (e) => {
|
link.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetId = link.getAttribute("href").substring(1);
|
const targetId = link.getAttribute("href").substring(1);
|
||||||
const targetSection = document.getElementById(targetId);
|
scrollToSection(targetId);
|
||||||
if (targetSection) {
|
|
||||||
targetSection.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
// Focus the section for accessibility
|
|
||||||
targetSection.setAttribute("tabindex", "-1");
|
|
||||||
targetSection.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Keyboard navigation inside nav list ──
|
// ── Keyboard navigation inside nav list ──
|
||||||
if (navList) {
|
navList.addEventListener("keydown", (e) => {
|
||||||
navList.addEventListener("keydown", (e) => {
|
const items = [...navList.querySelectorAll(".help-nav-link:not(.hidden)")];
|
||||||
const items = [...navList.querySelectorAll(".help-nav-link")];
|
if (!items.length) return;
|
||||||
const currentIdx = items.indexOf(document.activeElement);
|
const currentIdx = items.indexOf(document.activeElement);
|
||||||
|
|
||||||
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const next = (currentIdx + 1) % items.length;
|
const next = (currentIdx + 1) % items.length;
|
||||||
items[next].focus();
|
items[next].focus();
|
||||||
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = (currentIdx - 1 + items.length) % items.length;
|
const prev = (currentIdx - 1 + items.length) % items.length;
|
||||||
items[prev].focus();
|
items[prev].focus();
|
||||||
} else if (e.key === "Home") {
|
} else if (e.key === "Home") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
items[0].focus();
|
items[0].focus();
|
||||||
} else if (e.key === "End") {
|
} else if (e.key === "End") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
items[items.length - 1].focus();
|
items[items.length - 1].focus();
|
||||||
|
} else if (e.key === "Enter" && currentIdx >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = items[currentIdx].getAttribute("href").substring(1);
|
||||||
|
scrollToSection(targetId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make nav items focusable
|
||||||
|
navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
|
||||||
|
|
||||||
|
// ── Search filter ──
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener("input", () => {
|
||||||
|
const q = searchInput.value.toLowerCase().trim();
|
||||||
|
allLinks.forEach((link) => {
|
||||||
|
const text = link.textContent.toLowerCase();
|
||||||
|
if (!q || text.includes(q)) {
|
||||||
|
link.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
link.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Update active state for visible links
|
||||||
|
updateActiveLink();
|
||||||
|
});
|
||||||
|
// Clear search on Escape
|
||||||
|
searchInput.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
searchInput.value = "";
|
||||||
|
searchInput.dispatchEvent(new Event("input"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Make nav items focusable
|
// ── Mobile toggle ──
|
||||||
navList.querySelectorAll("li").forEach((li) => {
|
if (toggleBtn) {
|
||||||
li.setAttribute("role", "none"); // remove listitem role for cleaner a11y
|
toggleBtn.addEventListener("click", () => {
|
||||||
|
nav.classList.toggle("collapsed");
|
||||||
|
});
|
||||||
|
// Start collapsed on mobile
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
nav.classList.add("collapsed");
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (window.innerWidth > 768) {
|
||||||
|
nav.classList.remove("collapsed");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Scroll spy (IntersectionObserver) ──
|
||||||
|
let observer;
|
||||||
|
function updateActiveLink() {
|
||||||
|
// Find the first visible section and highlight its link
|
||||||
|
const sections = [...document.querySelectorAll(".help-section")];
|
||||||
|
let activeId = null;
|
||||||
|
for (const section of sections) {
|
||||||
|
const rect = section.getBoundingClientRect();
|
||||||
|
const helpContent = document.querySelector(".help-content");
|
||||||
|
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||||||
|
// Section top is in the upper half of the viewport (or within content)
|
||||||
|
if (rect.top < (contentRect ? contentRect.top + 120 : 200)) {
|
||||||
|
activeId = section.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeId) {
|
||||||
|
allLinks.forEach((link) => {
|
||||||
|
const isMatch = link.getAttribute("href") === `#${activeId}`;
|
||||||
|
link.classList.toggle("active", isMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
// Find the entry most visible near the top
|
||||||
|
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],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.querySelectorAll(".help-section").forEach((section) => {
|
||||||
|
observer.observe(section);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Keyboard: Ctrl+↑/↓ inside help body to jump between sections ──
|
// ── Keyboard: Ctrl+↑/↓ inside help body to jump between sections ──
|
||||||
helpBody.addEventListener("keydown", (e) => {
|
helpBody.addEventListener("keydown", (e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
||||||
@@ -269,10 +373,11 @@ function initHelpNavigation() {
|
|||||||
const sections = [...document.querySelectorAll(".help-section")];
|
const sections = [...document.querySelectorAll(".help-section")];
|
||||||
const currentSection = sections.find((s) => {
|
const currentSection = sections.find((s) => {
|
||||||
const rect = s.getBoundingClientRect();
|
const rect = s.getBoundingClientRect();
|
||||||
return rect.top >= 0 && rect.top < 100;
|
const helpContent = document.querySelector(".help-content");
|
||||||
|
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||||||
|
return rect.top >= (contentRect ? contentRect.top : 0) && rect.top < (contentRect ? contentRect.top + 120 : 120);
|
||||||
});
|
});
|
||||||
if (!currentSection) return;
|
if (!currentSection) return;
|
||||||
|
|
||||||
const currentIdx = sections.indexOf(currentSection);
|
const currentIdx = sections.indexOf(currentSection);
|
||||||
if (e.key === "ArrowDown" && currentIdx < sections.length - 1) {
|
if (e.key === "ArrowDown" && currentIdx < sections.length - 1) {
|
||||||
sections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" });
|
sections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
@@ -281,33 +386,6 @@ function initHelpNavigation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Scroll spy ──
|
|
||||||
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() {
|
function closeHelpModal() {
|
||||||
|
|||||||
+112
-11
@@ -2183,14 +2183,23 @@ select {
|
|||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
background: color-mix(in srgb, var(--bg-secondary) 94%, var(--bg-primary));
|
background: color-mix(in srgb, var(--bg-secondary) 94%, var(--bg-primary));
|
||||||
border-right: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
|
border-right: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
|
||||||
padding: 24px 0;
|
display: flex;
|
||||||
overflow: visible;
|
flex-direction: column;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
max-height: none;
|
height: 100%;
|
||||||
|
max-height: calc(90vh - 57px);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
backdrop-filter: blur(12px);
|
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 {
|
.help-nav-title {
|
||||||
font-family: "JetBrains Mono", monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
@@ -2199,27 +2208,62 @@ select {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding: 0 22px 14px;
|
|
||||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
.help-nav-toggle {
|
||||||
|
display: none;
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.help-nav-toggle:hover { color: var(--accent); border-color: var(--accent); }
|
||||||
|
.help-nav-search-wrap {
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.help-nav-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.help-nav-search:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.help-nav-search::placeholder { color: var(--text-muted); }
|
||||||
.help-nav-list {
|
.help-nav-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px 20px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
}
|
}
|
||||||
.help-nav-list li {
|
.help-nav-list li {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 3px;
|
||||||
}
|
}
|
||||||
.help-nav-link {
|
.help-nav-link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
.help-nav-link:hover {
|
.help-nav-link:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -2228,6 +2272,11 @@ select {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transform: translateX(2px);
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
.help-nav-link:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
.help-nav-link.active {
|
.help-nav-link.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, transparent), color-mix(in srgb, var(--accent) 7%, transparent));
|
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, transparent), color-mix(in srgb, var(--accent) 7%, transparent));
|
||||||
@@ -2235,6 +2284,9 @@ select {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
.help-nav-link.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.help-content {
|
.help-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -6335,3 +6387,52 @@ body.popup-mode .content-area {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Help Modal: Mobile responsive ── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.help-container { max-width: 100%; height: 100vh; }
|
||||||
|
.help-container .editor-container {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.help-nav-header {
|
||||||
|
padding: 12px 16px 8px;
|
||||||
|
}
|
||||||
|
.help-nav-toggle {
|
||||||
|
display: inline-flex; /* visible on mobile */
|
||||||
|
}
|
||||||
|
.help-nav.collapsed .help-nav-search-wrap,
|
||||||
|
.help-nav.collapsed .help-nav-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.help-nav-list {
|
||||||
|
max-height: 40vh;
|
||||||
|
flex: none;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.help-content {
|
||||||
|
padding: 16px 12px 40px;
|
||||||
|
}
|
||||||
|
.help-hero {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.help-hero-visual { display: none; }
|
||||||
|
.help-section { padding: 24px 0 20px; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user