Add sidebar TOC, simplify Données Secondaires section

- Rename 'Éditer Données Secondaires' → 'Données Secondaires', remove fieldset wrapper on Mots-clés link
- Create admin-toc.php partial: IntersectionObserver-based sidebar nav
- Include TOC on contenus.php, acces.php, parametres.php
- Add .admin-with-toc flex layout (sidebar + main) and .admin-toc CSS
- Fonts (Ductus, BBB DM Sans): verified loaded via variables.css → common.css import chain
- TOC: move inside <main> as <aside>, content in <article>, fix scrolling
- Lazy load: hx-trigger='load delay:100ms' with spinner (htmx-indicator) for tags/langues
- Inline rename: edit button in Nom cell, HTMX post for rename, validate+ cancel buttons
- Checkbox column: width:1% / fit-content
- Remove per-row merge forms/selects, only bulk merge when ≥2 checkboxes selected
- Remove per-row merge dialogs, keep only bulk merge and delete dialogs
- Add htmx-settling CSS transition for lazy-load fade-in
- Update acces.php/parametres.php: article layout, TOC inside main
- TOC: DOMContentLoaded guard, use <nav>+<a> directly instead of <ul>/<li>
- Section spacing: margin-bottom on sections and fieldsets in admin-main--toc
- Language dedup: GROUP BY LOWER(name) in getAllLanguagesWithCount and searchLanguages
- deduplicateLanguages() merges duplicate names and reassigns thesis_languages
- Sticky bulk-actions: position:sticky;top:0;z-index:10
- Tags toolbar: title left, stat count right (margin-left:auto), search bar under
- Tags count stat updated via hx-swap-oob from fragment
- Remove margin/max-width from .admin-main--toc
- Gap between TOC and article: --space-xs, sticky top: --space-xs
- Main padding: --space-s / --space-m / --space-xl (was --space-l/--space-l/--space-2xl)
- Article padding-top: --space-m
This commit is contained in:
Pontoporeia
2026-05-10 12:52:10 +02:00
parent 396cf19e9f
commit a3ded16915
12 changed files with 492 additions and 473 deletions

View File

@@ -2,77 +2,56 @@
/**
* admin-toc.php — sidebar table-of-contents for long admin pages.
*
* Scans <section aria-labelledby="..."> elements in #main-content and builds a
* slim vertical nav. Uses IntersectionObserver to highlight the active section.
*
* Usage: include APP_ROOT . '/templates/admin/partials/admin-toc.php';
* Rendered as an <aside> inside <main>, before the <article> content.
* Uses IntersectionObserver to highlight the active section.
*/
?>
<nav id="admin-toc" class="admin-toc" aria-label="Sur cette page">
<ul class="admin-toc-list" id="admin-toc-list">
<aside id="admin-toc" class="admin-toc" aria-label="Sur cette page">
<nav class="admin-toc-list" id="admin-toc-list">
<!-- populated by JS -->
</ul>
</nav>
</nav>
</aside>
<script>
(function() {
var main = document.getElementById('main-content');
if (!main) return;
function build() {
var main = document.getElementById('main-content');
var nav = document.getElementById('admin-toc-list');
var aside = document.getElementById('admin-toc');
if (!main || !nav || !aside) return;
var tocList = document.getElementById('admin-toc-list');
if (!tocList) return;
var sections = main.querySelectorAll('section[aria-labelledby]');
if (sections.length < 2) { aside.hidden = true; return; }
// Find all labelled sections
var sections = main.querySelectorAll('section[aria-labelledby]');
if (sections.length < 2) {
document.getElementById('admin-toc').style.display = 'none';
return;
var items = [];
sections.forEach(function(sec) {
var headingId = sec.getAttribute('aria-labelledby');
var heading = document.getElementById(headingId);
if (!heading) return;
if (!sec.id) sec.id = headingId;
var a = document.createElement('a');
a.href = '#' + sec.id;
a.textContent = heading.textContent.trim();
a.style.display = 'block';
nav.appendChild(a);
items.push({ section: sec, link: a });
});
var observer = new IntersectionObserver(function(entries) {
var best = null, bestRatio = 0;
entries.forEach(function(e) {
if (e.intersectionRatio > bestRatio) { bestRatio = e.intersectionRatio; best = e.target; }
});
items.forEach(function(item) {
item.link.classList.toggle('admin-toc-active', item.section === best);
});
}, { rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] });
items.forEach(function(item) { observer.observe(item.section); });
}
var items = [];
sections.forEach(function(sec) {
var headingId = sec.getAttribute('aria-labelledby');
var heading = document.getElementById(headingId);
if (!heading) return;
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '#' + sec.id;
a.textContent = heading.textContent;
a.setAttribute('data-toc-target', sec.id);
li.appendChild(a);
tocList.appendChild(li);
// Ensure section has an id for anchoring
if (!sec.id) {
sec.id = headingId;
}
items.push({ section: sec, link: a });
});
// IntersectionObserver: highlight the link whose section is most visible
var observer = new IntersectionObserver(function(entries) {
var best = null;
var bestRatio = 0;
entries.forEach(function(e) {
if (e.intersectionRatio > bestRatio) {
bestRatio = e.intersectionRatio;
best = e.target;
}
});
if (best) {
items.forEach(function(item) {
var isActive = item.section === best;
item.link.classList.toggle('admin-toc-active', isActive);
});
}
}, {
rootMargin: '-10% 0px -70% 0px',
threshold: [0, 0.25, 0.5, 0.75, 1]
});
items.forEach(function(item) { observer.observe(item.section); });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', build);
else build();
})();
</script>