Fix biome lint errors: remove duplicate CSS properties, apply safe auto-fixes

CSS:
- Remove duplicate 'background' fallbacks in base.css, header.css, search.css
  (solid color declared before gradient — gradient always wins)
- Remove duplicate 'padding' in admin.css .admin-import-log

JS (biome --write safe fixes applied):
- function() → arrow functions in all IIFEs and callbacks
- forEach/callback → arrow functions
- evaluePtrn → parseInt(x, 10) in admin-contacts-form.js
- Cleaned label text in build.mjs lint step

Remaining warnings are intentional: !important overrides, descending
specificity (admin.css cascade), noUnusedVariables (functions exported
to window/onclick), useTemplate style preference.
This commit is contained in:
Pontoporeia
2026-06-24 13:21:04 +02:00
parent 82d3dcb084
commit 6ecd3d4540
31 changed files with 336 additions and 348 deletions

View File

@@ -1198,7 +1198,6 @@ th.admin-ap-col {
.admin-import-log { .admin-import-log {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-3xs); gap: var(--space-3xs);
@@ -2158,18 +2157,23 @@ th.admin-ap-col {
100% { transform: scaleX(0); transform-origin: right; } 100% { transform: scaleX(0); transform-origin: right; }
} }
/* ── Sidebar TOC ───────────────────────────────────────────────────────────── */ /* ── Sidebar TOC (matches public .page-content alignment pattern) ────────── */
.admin-main--toc { .admin-main--toc {
display: flex; display: flex;
gap: var(--space-xs); gap: var(--space-2xl);
align-items: flex-start; align-items: start;
padding: var(--space-xl) var(--space-m) var(--space-2xl);
} }
.admin-main--toc > article { .admin-main--toc > article {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding-top: var(--space-m); }
/* Align first child of article with TOC heading (same as public) */
.admin-main--toc > article > :first-child {
margin-top: 2.2rem;
} }
.admin-main--toc > article > section { .admin-main--toc > article > section {
@@ -2180,41 +2184,14 @@ th.admin-ap-col {
margin-bottom: var(--space-m); margin-bottom: var(--space-m);
} }
.admin-toc { /* Admin TOC: same <details class="toc"> as public pages, positioned sticky */
position: sticky; #admin-toc {
top: var(--space-xs); width: 180px;
width: 160px;
flex-shrink: 0; flex-shrink: 0;
padding-top: var(--space-m);
} }
.admin-toc-list { #admin-toc .toc-list a {
list-style: none; font-size: var(--step--1);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.admin-toc-list a {
display: block;
padding: var(--space-3xs) var(--space-2xs);
font-size: var(--step--2);
color: var(--text-secondary);
text-decoration: none;
border-left: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.admin-toc-list a:hover {
color: var(--text-primary);
}
.admin-toc-list a.admin-toc-active {
color: var(--text-primary);
font-weight: 600;
border-left-color: var(--accent, var(--color-primary));
} }
/* ── Lazy-load transition ─────────────────────────────────────────────────── */ /* ── Lazy-load transition ─────────────────────────────────────────────────── */

View File

@@ -39,7 +39,6 @@ html, body {
body { body {
font-family: var(--font-body); font-family: var(--font-body);
font-weight: 300; font-weight: 300;
background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
background: linear-gradient( background: linear-gradient(
180deg, 180deg,

View File

@@ -8,7 +8,6 @@
header { header {
vertical-align: center; vertical-align: center;
flex-shrink: 0; flex-shrink: 0;
background: #c05de1;
background: linear-gradient( background: linear-gradient(
0deg, 0deg,
rgba(192, 93, 225, 1) 0%, rgba(192, 93, 225, 1) 0%,

View File

@@ -21,7 +21,6 @@ form[role="search"]:not(.header-search-form) {
.header-search-wrap { .header-search-wrap {
padding: 0; padding: 0;
flex-shrink: 0; flex-shrink: 0;
background: #C05DE1;
background: linear-gradient(180deg, rgba(192, 93, 225, 1) 0%, rgba(255, 255, 255, 1) 100%); background: linear-gradient(180deg, rgba(192, 93, 225, 1) 0%, rgba(255, 255, 255, 1) 100%);
} }

View File

@@ -0,0 +1,141 @@
/* ============================================================
SHARED TOC — sidebar table-of-contents for admin + public pages.
Both use <details class="toc"> markup.
Public: server-rendered links (about, charte, licence)
Admin: JS-populated links (contenus, accès, paramètres — admin-toc.js)
============================================================ */
/* ── Shared list styles (.toc-list) ─────────────────────────────────────── */
.toc-list,
.toc ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3xs);
}
.toc-list a,
.toc ul a {
display: block;
font-family: var(--font-body);
font-size: var(--step-0);
font-weight: 300;
color: var(--text-primary);
text-decoration: none;
padding: var(--space-3xs) 0;
transition: color 0.15s;
}
.toc-list a:hover {
color: var(--accent-primary);
}
.toc-list a.toc-active,
.toc ul a.toc-active {
color: var(--accent-primary);
font-weight: 400;
}
/* ── Public TOC (details/summary block) ─────────────────────────────────── */
.toc {
border: none;
border-radius: 0;
background: transparent;
overflow: visible;
}
.toc[open] {
padding-bottom: 0;
}
.toc > :not(summary) {
padding-left: 0;
padding-right: 0;
}
.toc summary {
font-family: var(--font-display);
font-size: var(--step-1);
font-weight: 400;
color: var(--text-primary);
padding: 0 0 var(--space-2xs) 0;
border-bottom: 1px solid var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2xs);
background: transparent;
cursor: pointer;
list-style: none;
}
.toc summary::-webkit-details-marker {
display: none;
}
.toc summary:hover {
color: inherit;
background: transparent;
}
/* Caret icon inside summary — visible only on mobile */
.toc-caret {
flex-shrink: 0;
transition: transform 0.2s ease;
}
.toc[open] > summary .toc-caret {
transform: rotate(180deg);
}
/* Sidebar links (about page — custom links below TOC) */
.toc-sidebar-link:first-of-type {
padding-top: var(--space-s);
border-top: 1px solid var(--border-primary);
}
.toc-sidebar-link + .toc-sidebar-link {
padding-top: var(--space-xs);
}
.toc-sidebar-link a {
font-size: var(--step--2);
color: var(--accent-primary);
text-decoration: none;
transition: opacity 0.15s;
}
.toc-sidebar-link a:hover {
opacity: 0.8;
}
/* ── Desktop: sticky sidebar ───────────────────────────────────────────── */
@media (min-width: 768px) {
.toc {
position: sticky;
top: var(--space-l);
grid-column: 1;
}
.toc summary {
pointer-events: none;
}
.toc-caret {
display: none;
}
}
/* ── Mobile: collapsible TOC ────────────────────────────────────────────── */
@media (max-width: 767px) {
.toc {
position: static;
grid-column: 1;
}
.toc summary {
cursor: pointer;
list-style: revert;
}
}

View File

@@ -21,142 +21,17 @@
align-items: start; align-items: start;
} }
/* ------------------------------------------------------------------ */ /* ── TOC styles → components/toc.css (shared with admin) ───────────────── */
/* Table of contents — details/summary block */
/* ------------------------------------------------------------------ */
.toc {
border: none;
border-radius: 0;
background: transparent;
overflow: visible;
}
.toc[open] {
padding-bottom: 0;
}
.toc > :not(summary) {
padding-left: 0;
padding-right: 0;
}
.toc summary {
font-family: var(--font-display);
font-size: var(--step-1);
font-weight: 400;
color: var(--text-primary);
padding: 0 0 var(--space-2xs) 0;
border-bottom: 1px solid var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2xs);
background: transparent;
cursor: pointer;
list-style: none;
}
.toc summary::-webkit-details-marker {
display: none;
}
.toc summary:hover {
color: inherit;
background: transparent;
}
/* Caret icon inside summary — visible only on mobile */
.toc-caret {
flex-shrink: 0;
transition: transform 0.2s ease;
}
.toc[open] > summary .toc-caret {
transform: rotate(180deg);
}
.toc ul {
list-style: none;
margin: var(--space-xs) 0 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3xs);
}
.toc ul a {
font-family: var(--font-body);
font-size: var(--step-0);
font-weight: 300;
color: var(--text-primary);
text-decoration: none;
display: block;
padding: var(--space-3xs) 0;
transition: color 0.15s;
}
.toc ul a:hover {
color: var(--accent-primary);
}
/* Sidebar links (about page) */
.toc-sidebar-link:first-of-type {
padding-top: var(--space-s);
border-top: 1px solid var(--border-primary);
}
.toc-sidebar-link + .toc-sidebar-link {
padding-top: var(--space-xs);
}
.toc-sidebar-link a {
font-size: var(--step--2);
color: var(--accent-primary);
text-decoration: none;
transition: opacity 0.15s;
}
.toc-sidebar-link a:hover {
color: var(--accent-primary);
opacity: 1;
}
/* ------------------------------------------------------------------ */
/* Desktop: force-open TOC, hide caret, sticky sidebar */
/* ------------------------------------------------------------------ */
/* Grid column positioning for content pages with sidebar TOC */
@media (min-width: 768px) { @media (min-width: 768px) {
.toc { .page-content > article {
position: sticky;
top: var(--space-l);
grid-column: 1;
}
.toc summary {
pointer-events: none;
list-style: none;
}
.toc summary::-webkit-details-marker {
display: none;
}
.toc-caret {
display: none;
}
.page-content > .content,
.page-content > .content-section {
grid-column: 2; grid-column: 2;
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
} }
} }
/* ------------------------------------------------------------------ */
/* Mobile: collapsible TOC */
/* ------------------------------------------------------------------ */
@media (max-width: 767px) { @media (max-width: 767px) {
.page-content { .page-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -164,30 +39,25 @@
padding: var(--space-m) var(--space-s) var(--space-xl); padding: var(--space-m) var(--space-s) var(--space-xl);
} }
.page-content > article {
grid-column: 1;
}
.toc { .toc {
margin-top: var(--space-s); margin-top: var(--space-s);
position: static;
grid-column: 1;
}
.page-content > .content,
.page-content > .content-section {
grid-column: 1;
}
.toc summary {
cursor: pointer;
list-style: revert;
} }
} }
/* ── First content child aligns with TOC heading top ───────────────────── */
.page-content > article > :first-child {
margin-top: 2.2rem;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Main content area */ /* Article content typography */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Shared typography for about-page sections and charte/licence content */ .page-content > article {
.content,
.content-section {
display: block; display: block;
max-width: 100%; max-width: 100%;
font-family: var(--font-body); font-family: var(--font-body);
@@ -195,77 +65,57 @@
line-height: 1.6; line-height: 1.6;
color: var(--text-primary); color: var(--text-primary);
font-weight: 300; font-weight: 300;
padding-bottom: var(--space-xl);
} }
.content *, .page-content > article * {
.content-section * {
max-width: 100%; max-width: 100%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;
} }
.content { .page-content > article p {
padding-bottom: var(--space-xl);
}
.content p,
.content-section p {
margin: 0 0 1em 0; margin: 0 0 1em 0;
} }
.content p:last-child, .page-content > article p:last-child {
.content-section p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.content :where(h1, h2, h3), .page-content > article :where(h1, h2, h3) {
.content-section :where(h1, h2, h3) {
margin: 1.5em 0 0.5em 0; margin: 1.5em 0 0.5em 0;
} }
.content :where(h1, h2, h3):first-child, .page-content > article a {
.content-section :where(h1, h2, h3):first-child {
margin-top: 2.2rem;
}
.content a,
.content-section a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
font-weight: 700; font-weight: 700;
} }
.content a:hover, .page-content > article a:hover {
.content-section a:hover {
color: var(--accent-primary); color: var(--accent-primary);
text-decoration: none; text-decoration: none;
} }
.content ul, .page-content > article ul,
.content ol, .page-content > article ol {
.content-section ul,
.content-section ol {
padding-left: var(--space-m); padding-left: var(--space-m);
margin-bottom: var(--space-s); margin-bottom: var(--space-s);
} }
.content li, .page-content > article li {
.content-section li {
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
.content strong, .page-content > article strong {
.content-section strong {
font-weight: 700; font-weight: 700;
} }
.content em, .page-content > article em {
.content-section em {
font-style: italic; font-style: italic;
} }
.content :where(pre, pre code, code), .page-content > article :where(pre, pre code, code) {
.content-section :where(pre, pre code, code) {
display: block; display: block;
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
@@ -288,19 +138,29 @@
max-width: 100%; max-width: 100%;
} }
/* Section separators (about page only — .content-section adds dividers) */ /* Section separators (about page only) */
.page-content > .content-section { .page-content > article > section {
padding-bottom: var(--space-xl); padding-bottom: var(--space-xl);
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
margin-bottom: var(--space-xl); margin-bottom: var(--space-xl);
} }
.page-content > .content-section:last-child { .page-content > article > section:last-child {
border-bottom: none; border-bottom: none;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: var(--space-xl); padding-bottom: var(--space-xl);
} }
/* Scroll margin so anchor links account for the sticky header */
.page-content > article :where(h1, h2, h3) {
scroll-margin-top: var(--space-l);
}
/* Hide CommonMark heading permalink anchors */
.heading-permalink {
display: none;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Section titles */ /* Section titles */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -314,17 +174,6 @@
line-height: 1.1; line-height: 1.1;
} }
/* Scroll margin so anchor links account for the sticky header */
.content :where(h1, h2, h3),
.content-section :where(h1, h2, h3) {
scroll-margin-top: var(--space-l);
}
/* Hide CommonMark heading permalink anchors (id now lives on the heading itself) */
.heading-permalink {
display: none;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Contacts grid */ /* Contacts grid */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -431,7 +280,7 @@
padding: var(--space-m) var(--space-s) var(--space-xl); padding: var(--space-m) var(--space-s) var(--space-xl);
} }
.content-section { .page-content > article {
font-size: var(--step-0); font-size: var(--step-0);
} }

View File

@@ -23,5 +23,6 @@
@import "components/pagination.css"; @import "components/pagination.css";
@import "components/header.css"; @import "components/header.css";
@import "components/search.css"; @import "components/search.css";
@import "components/toc.css";
@import "utilities.css"; @import "utilities.css";

View File

@@ -3,10 +3,10 @@
* *
* Provides: dialog openers, clipboard copy, password dialog. * Provides: dialog openers, clipboard copy, password dialog.
*/ */
(function () { (() => {
var createBtn = document.getElementById('open-create-dialog'); var createBtn = document.getElementById('open-create-dialog');
if (createBtn) { if (createBtn) {
createBtn.addEventListener('click', function () { createBtn.addEventListener('click', () => {
document.getElementById('create-dialog').showModal(); document.getElementById('create-dialog').showModal();
}); });
} }
@@ -26,11 +26,11 @@ function _executeDeleteLink() {
function copyUrl(id) { function copyUrl(id) {
var input = document.getElementById('url-' + id); var input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(function () { navigator.clipboard.writeText(input.value).then(() => {
var btn = event.target.closest('button'); var btn = event.target.closest('button');
var orig = btn.textContent; var orig = btn.textContent;
btn.textContent = '✓ Copié'; btn.textContent = '✓ Copié';
setTimeout(function () { setTimeout(() => {
btn.textContent = orig; btn.textContent = orig;
}, 1200); }, 1200);
}); });

View File

@@ -6,7 +6,7 @@
* <meta name="acces-new-link-password" content="..."> * <meta name="acces-new-link-password" content="...">
* <meta name="acces-new-link-slug" content="..."> * <meta name="acces-new-link-slug" content="...">
*/ */
(function () { (() => {
var baseUrlMeta = document.querySelector('meta[name="acces-base-url"]'); var baseUrlMeta = document.querySelector('meta[name="acces-base-url"]');
var passwordMeta = document.querySelector('meta[name="acces-new-link-password"]'); var passwordMeta = document.querySelector('meta[name="acces-new-link-password"]');
var slugMeta = document.querySelector('meta[name="acces-new-link-slug"]'); var slugMeta = document.querySelector('meta[name="acces-new-link-slug"]');
@@ -22,7 +22,7 @@
// Create dialog opener // Create dialog opener
var createBtn = document.getElementById('open-create-dialog'); var createBtn = document.getElementById('open-create-dialog');
if (createBtn) { if (createBtn) {
createBtn.addEventListener('click', function () { createBtn.addEventListener('click', () => {
document.getElementById('create-dialog').showModal(); document.getElementById('create-dialog').showModal();
}); });
} }
@@ -30,12 +30,12 @@
function copyUrl(id) { function copyUrl(id) {
var input = document.getElementById('url-' + id); var input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(function () { navigator.clipboard.writeText(input.value).then(() => {
var btn = event.target.closest('button'); var btn = event.target.closest('button');
if (btn) { if (btn) {
var orig = btn.getAttribute('title') || ''; var orig = btn.getAttribute('title') || '';
btn.setAttribute('title', '✓ Copié'); btn.setAttribute('title', '✓ Copié');
setTimeout(function () { setTimeout(() => {
btn.setAttribute('title', orig); btn.setAttribute('title', orig);
}, 1200); }, 1200);
} }
@@ -43,12 +43,12 @@ function copyUrl(id) {
} }
function copyUrlFrom(el) { function copyUrlFrom(el) {
navigator.clipboard.writeText(el.value).then(function () { navigator.clipboard.writeText(el.value).then(() => {
var btn = el.nextElementSibling; var btn = el.nextElementSibling;
if (btn) { if (btn) {
var orig = btn.textContent; var orig = btn.textContent;
btn.textContent = '✓ Copié'; btn.textContent = '✓ Copié';
setTimeout(function () { setTimeout(() => {
btn.textContent = orig; btn.textContent = orig;
}, 1200); }, 1200);
} }
@@ -57,17 +57,17 @@ function copyUrlFrom(el) {
function copyTextToClipboard(text) { function copyTextToClipboard(text) {
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
.then(function () { .then(() => {
var btn = event && event.target ? event.target.closest('button') : null; var btn = event && event.target ? event.target.closest('button') : null;
if (btn) { if (btn) {
var orig = btn.getAttribute('title') || ''; var orig = btn.getAttribute('title') || '';
btn.setAttribute('title', '✓ Copié'); btn.setAttribute('title', '✓ Copié');
setTimeout(function () { setTimeout(() => {
btn.setAttribute('title', orig); btn.setAttribute('title', orig);
}, 1200); }, 1200);
} }
}) })
.catch(function () {}); .catch(() => {});
} }
function openEditDialog(id, name, hasPassword, expiresVal) { function openEditDialog(id, name, hasPassword, expiresVal) {

View File

@@ -4,7 +4,7 @@
* Reads `data-apropos-key` and `data-apropos-group-count` from the form element. * Reads `data-apropos-key` and `data-apropos-group-count` from the form element.
* Expects templates with ids `entry-template-f-{key}` and `group-template-f-{key}`. * Expects templates with ids `entry-template-f-{key}` and `group-template-f-{key}`.
*/ */
(function () { (() => {
var form = document.querySelector('form[data-apropos-key]'); var form = document.querySelector('form[data-apropos-key]');
if (!form) return; if (!form) return;
@@ -16,12 +16,12 @@
function reindexGroups() { function reindexGroups() {
var fieldsets = form.querySelectorAll('fieldset.apropos-group'); var fieldsets = form.querySelectorAll('fieldset.apropos-group');
groupCount = fieldsets.length; groupCount = fieldsets.length;
fieldsets.forEach(function (fs, i) { fieldsets.forEach((fs, i) => {
var newIdx = i; var newIdx = i;
var legend = fs.querySelector('legend'); var legend = fs.querySelector('legend');
if (legend) legend.textContent = 'Contact ' + (newIdx + 1); if (legend) legend.textContent = 'Contact ' + (newIdx + 1);
fs.querySelectorAll('input').forEach(function (inp) { fs.querySelectorAll('input').forEach((inp) => {
if (inp.name) { if (inp.name) {
inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']'); inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']');
} }
@@ -33,7 +33,7 @@
} }
}); });
fs.querySelectorAll('label[for]').forEach(function (lbl) { fs.querySelectorAll('label[for]').forEach((lbl) => {
lbl.setAttribute( lbl.setAttribute(
'for', 'for',
lbl.getAttribute('for').replace( lbl.getAttribute('for').replace(

View File

@@ -18,7 +18,7 @@ function languesSubmitPending() {
} }
function languesToggleAll(src) { function languesToggleAll(src) {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_langs[]"]').forEach((cb) => {
cb.checked = src.checked; cb.checked = src.checked;
}); });
languesUpdateBulk(); languesUpdateBulk();
@@ -32,7 +32,7 @@ function languesUpdateBulk() {
var visible = n > 1; var visible = n > 1;
bar.style.display = visible ? 'flex' : 'none'; bar.style.display = visible ? 'flex' : 'none';
if (visible) { if (visible) {
requestAnimationFrame(function () { requestAnimationFrame(() => {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px'); wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
}); });
} else { } else {
@@ -41,7 +41,7 @@ function languesUpdateBulk() {
} }
function languesCancelSelection() { function languesCancelSelection() {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_langs[]"]').forEach((cb) => {
cb.checked = false; cb.checked = false;
}); });
languesUpdateBulk(); languesUpdateBulk();
@@ -57,7 +57,7 @@ function languesConfirmBulkDelete() {
function languesExecBulkDelete() { function languesExecBulkDelete() {
var container = document.getElementById('langues-bulk-checkboxes'); var container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = ''; container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) { document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach((cb) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_langs[]'; inp.name = 'selected_langs[]';
@@ -76,7 +76,7 @@ function languesConfirmBulkMerge() {
document.getElementById('langues-bulk-merge-count').textContent = checked.length; document.getElementById('langues-bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('langues-bulk-merge-target-select'); var sel = document.getElementById('langues-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>'; sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) { checked.forEach((cb) => {
var tr = cb.closest('tr'); var tr = cb.closest('tr');
sel.innerHTML += sel.innerHTML +=
'<option value="' + '<option value="' +
@@ -94,7 +94,7 @@ function languesExecBulkMerge() {
document.getElementById('langues-bulk-target').value = targetId; document.getElementById('langues-bulk-target').value = targetId;
var container = document.getElementById('langues-bulk-checkboxes'); var container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = ''; container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) { document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach((cb) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_langs[]'; inp.name = 'selected_langs[]';
@@ -105,11 +105,11 @@ function languesExecBulkMerge() {
document.getElementById('langues-bulk-form').submit(); document.getElementById('langues-bulk-form').submit();
} }
document.addEventListener('htmx:afterSwap', function (evt) { document.addEventListener('htmx:afterSwap', (evt) => {
if (evt.target.id === 'langues-table-wrap') { if (evt.target.id === 'langues-table-wrap') {
document document
.querySelectorAll('input[name="selected_langs[]"]') .querySelectorAll('input[name="selected_langs[]"]')
.forEach(function (cb) { .forEach((cb) => {
cb.addEventListener('change', languesUpdateBulk); cb.addEventListener('change', languesUpdateBulk);
}); });
languesUpdateBulk(); languesUpdateBulk();

View File

@@ -18,7 +18,7 @@ function motsclesSubmitPending() {
} }
function motsclesToggleAll(src) { function motsclesToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]').forEach((cb) => {
cb.checked = src.checked; cb.checked = src.checked;
}); });
motsclesUpdateBulk(); motsclesUpdateBulk();
@@ -32,7 +32,7 @@ function motsclesUpdateBulk() {
var visible = n > 1; var visible = n > 1;
bar.style.display = visible ? 'flex' : 'none'; bar.style.display = visible ? 'flex' : 'none';
if (visible) { if (visible) {
requestAnimationFrame(function () { requestAnimationFrame(() => {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px'); wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
}); });
} else { } else {
@@ -46,7 +46,7 @@ function motsclesConfirmBulkMerge() {
document.getElementById('motscles-bulk-merge-count').textContent = checked.length; document.getElementById('motscles-bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('motscles-bulk-merge-target-select'); var sel = document.getElementById('motscles-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>'; sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) { checked.forEach((cb) => {
var tr = cb.closest('tr'); var tr = cb.closest('tr');
sel.innerHTML += sel.innerHTML +=
'<option value="' + '<option value="' +
@@ -64,7 +64,7 @@ function motsclesExecBulkMerge() {
document.getElementById('motscles-bulk-target').value = targetId; document.getElementById('motscles-bulk-target').value = targetId;
var container = document.getElementById('motscles-bulk-checkboxes'); var container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = ''; container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach((cb) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_tags[]'; inp.name = 'selected_tags[]';
@@ -76,7 +76,7 @@ function motsclesExecBulkMerge() {
} }
function motsclesCancelSelection() { function motsclesCancelSelection() {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]').forEach((cb) => {
cb.checked = false; cb.checked = false;
}); });
motsclesUpdateBulk(); motsclesUpdateBulk();
@@ -92,7 +92,7 @@ function motsclesConfirmBulkDelete() {
function motsclesExecBulkDelete() { function motsclesExecBulkDelete() {
var container = document.getElementById('motscles-bulk-checkboxes'); var container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = ''; container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach((cb) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_tags[]'; inp.name = 'selected_tags[]';
@@ -105,11 +105,11 @@ function motsclesExecBulkDelete() {
document.getElementById('motscles-bulk-form').submit(); document.getElementById('motscles-bulk-form').submit();
} }
document.addEventListener('htmx:afterSwap', function (evt) { document.addEventListener('htmx:afterSwap', (evt) => {
if (evt.target.id === 'motscles-table-wrap') { if (evt.target.id === 'motscles-table-wrap') {
document document
.querySelectorAll('input[name="selected_tags[]"]') .querySelectorAll('input[name="selected_tags[]"]')
.forEach(function (cb) { .forEach((cb) => {
cb.addEventListener('change', motsclesUpdateBulk); cb.addEventListener('change', motsclesUpdateBulk);
}); });
motsclesUpdateBulk(); motsclesUpdateBulk();

View File

@@ -4,18 +4,20 @@
* Provides: toggleAll, updateBulk, getSelectedIds, confirmBulk, execBulk, * Provides: toggleAll, updateBulk, getSelectedIds, confirmBulk, execBulk,
* confirmExport, confirmExportFiles, confirmDelete. * confirmExport, confirmExportFiles, confirmDelete.
*/ */
(function () { (() => {
function toggleAll(src) { function toggleAll(src) {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_theses[]"]').forEach((cb) => {
cb.checked = src.checked; cb.checked = src.checked;
}); });
updateBulk(); updateBulk();
} }
function updateBulk() { function updateBulk() {
var n = document.querySelectorAll('input[name="selected_theses[]"]:checked').length;
var b = document.getElementById('bulk-actions'); var b = document.getElementById('bulk-actions');
document.getElementById('selected-count').textContent = n; var c = document.getElementById('selected-count');
if (!b || !c) return; // only on thesis index page
var n = document.querySelectorAll('input[name="selected_theses[]"]:checked').length;
c.textContent = n;
b.style.display = n > 0 ? 'flex' : 'none'; b.style.display = n > 0 ? 'flex' : 'none';
document.getElementById('admin-table-wrap').style.setProperty( document.getElementById('admin-table-wrap').style.setProperty(
'--sticky-top', '--sticky-top',
@@ -26,15 +28,15 @@
function getSelectedIds() { function getSelectedIds() {
return Array.from( return Array.from(
document.querySelectorAll('input[name="selected_theses[]"]:checked') document.querySelectorAll('input[name="selected_theses[]"]:checked')
).map(function (cb) { ).map((cb) => cb.value);
return cb.value;
});
} }
function confirmBulk(act) { function confirmBulk(act) {
var noSel = document.getElementById('no-selection-dialog');
if (!noSel) return; // only on thesis index page
var ids = getSelectedIds(); var ids = getSelectedIds();
if (!ids.length) { if (!ids.length) {
document.getElementById('no-selection-dialog').showModal(); noSel.showModal();
return; return;
} }
var n = ids.length; var n = ids.length;
@@ -56,7 +58,7 @@
f.action = a === 'delete' ? 'actions/delete.php' : 'actions/publish.php'; f.action = a === 'delete' ? 'actions/delete.php' : 'actions/publish.php';
var c = document.getElementById('bulk-checkboxes'); var c = document.getElementById('bulk-checkboxes');
c.innerHTML = ''; c.innerHTML = '';
getSelectedIds().forEach(function (id) { getSelectedIds().forEach((id) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_theses[]'; inp.name = 'selected_theses[]';
@@ -85,15 +87,17 @@
} }
function confirmDelete(id, title) { function confirmDelete(id, title) {
var dialog = document.getElementById('delete-thesis-dialog');
if (!dialog) return; // only on thesis index page
document.getElementById('delete-thesis-title').textContent = title; document.getElementById('delete-thesis-title').textContent = title;
document.getElementById('delete-thesis-dialog').showModal(); dialog.showModal();
document.getElementById('delete-dialog-confirm').onclick = function () { document.getElementById('delete-dialog-confirm').onclick = () => {
document.getElementById('delete-form-' + id).submit(); document.getElementById('delete-form-' + id).submit();
}; };
} }
function reattachListeners() { function reattachListeners() {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_theses[]"]').forEach((cb) => {
cb.addEventListener('change', updateBulk); cb.addEventListener('change', updateBulk);
}); });
updateBulk(); updateBulk();

View File

@@ -11,7 +11,7 @@
var _pendingTagForm = null; var _pendingTagForm = null;
function tagsToggleAll(src) { function tagsToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]').forEach((cb) => {
cb.checked = src.checked; cb.checked = src.checked;
}); });
tagsUpdateBulk(); tagsUpdateBulk();
@@ -29,7 +29,7 @@ function tagsConfirmBulkMerge() {
document.getElementById('bulk-merge-count').textContent = checked.length; document.getElementById('bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('bulk-merge-target-select'); var sel = document.getElementById('bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>'; sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) { checked.forEach((cb) => {
var tr = cb.closest('tr'); var tr = cb.closest('tr');
sel.innerHTML += sel.innerHTML +=
'<option value="' + '<option value="' +
@@ -47,7 +47,7 @@ function tagsExecBulkMerge() {
document.getElementById('tags-bulk-target').value = targetId; document.getElementById('tags-bulk-target').value = targetId;
var container = document.getElementById('tags-bulk-checkboxes'); var container = document.getElementById('tags-bulk-checkboxes');
container.innerHTML = ''; container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach((cb) => {
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.type = 'hidden'; inp.type = 'hidden';
inp.name = 'selected_tags[]'; inp.name = 'selected_tags[]';
@@ -68,11 +68,11 @@ function _submitPendingTagForm() {
if (_pendingTagForm) _pendingTagForm.submit(); if (_pendingTagForm) _pendingTagForm.submit();
} }
document.addEventListener('htmx:afterSwap', function (evt) { document.addEventListener('htmx:afterSwap', (evt) => {
if (evt.target.id === 'tags-table-wrap') { if (evt.target.id === 'tags-table-wrap') {
document document
.querySelectorAll('input[name="selected_tags[]"]') .querySelectorAll('input[name="selected_tags[]"]')
.forEach(function (cb) { .forEach((cb) => {
cb.addEventListener('change', tagsUpdateBulk); cb.addEventListener('change', tagsUpdateBulk);
}); });
tagsUpdateBulk(); tagsUpdateBulk();

View File

@@ -1,33 +1,35 @@
/** /**
* admin-toc.js — Sticky table-of-contents with IntersectionObserver active-section * admin-toc.js — Table-of-contents with IntersectionObserver active-section
* highlighting. * highlighting.
* *
* Renders nav links from section[aria-labelledby] headings inside #main-content. * Shared markup with public content pages: <details class="toc" id="admin-toc">
* Hides the TOC aside if fewer than 2 sections exist. * contains a <ul class="toc-list"> populated by this script from
* section[aria-labelledby] headings inside #main-content.
* Hides the TOC if fewer than 2 sections exist.
* *
* Guarded: only runs once even if loaded multiple times (e.g. via bundle + direct <script>). * Guarded: only runs once.
*/ */
(function () { (() => {
if (window.__adminTocBuilt) return; if (window.__adminTocBuilt) return;
window.__adminTocBuilt = true; window.__adminTocBuilt = true;
function build() { function build() {
var main = document.getElementById('main-content'); var main = document.getElementById('main-content');
var nav = document.getElementById('admin-toc-list'); var toc = document.getElementById('admin-toc');
var aside = document.getElementById('admin-toc'); var list = document.getElementById('admin-toc-list');
if (!main || !nav || !aside) return; if (!main || !toc || !list) return;
// Guard against double population (nav already has children from a prior run) // Guard against double population
if (nav.children.length > 0) return; if (list.children.length > 0) return;
var sections = main.querySelectorAll('section[aria-labelledby]'); var sections = main.querySelectorAll('section[aria-labelledby]');
if (sections.length < 2) { if (sections.length < 2) {
aside.hidden = true; toc.hidden = true;
return; return;
} }
var items = []; var items = [];
sections.forEach(function (sec) { sections.forEach((sec) => {
var headingId = sec.getAttribute('aria-labelledby'); var headingId = sec.getAttribute('aria-labelledby');
var heading = document.getElementById(headingId); var heading = document.getElementById(headingId);
if (!heading) return; if (!heading) return;
@@ -36,29 +38,31 @@
var a = document.createElement('a'); var a = document.createElement('a');
a.href = '#' + sec.id; a.href = '#' + sec.id;
a.textContent = heading.textContent.trim(); a.textContent = heading.textContent.trim();
a.style.display = 'block';
nav.appendChild(a); var li = document.createElement('li');
li.appendChild(a);
list.appendChild(li);
items.push({ section: sec, link: a }); items.push({ section: sec, link: a });
}); });
var observer = new IntersectionObserver( var observer = new IntersectionObserver(
function (entries) { (entries) => {
var best = null, var best = null,
bestRatio = 0; bestRatio = 0;
entries.forEach(function (e) { entries.forEach((e) => {
if (e.intersectionRatio > bestRatio) { if (e.intersectionRatio > bestRatio) {
bestRatio = e.intersectionRatio; bestRatio = e.intersectionRatio;
best = e.target; best = e.target;
} }
}); });
items.forEach(function (item) { items.forEach((item) => {
item.link.classList.toggle('admin-toc-active', item.section === best); item.link.classList.toggle('toc-active', item.section === best);
}); });
}, },
{ rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] } { rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] }
); );
items.forEach(function (item) { items.forEach((item) => {
observer.observe(item.section); observer.observe(item.section);
}); });
} }

View File

@@ -699,7 +699,7 @@
// The format checkboxes no longer trigger HTMX swaps; this JS toggles the TFE // The format checkboxes no longer trigger HTMX swaps; this JS toggles the TFE
// required attribute and asterisk client-side so the student sees immediate feedback. // required attribute and asterisk client-side so the student sees immediate feedback.
// admin_mode hidden input (value="1") suppresses required toggling for admins. // admin_mode hidden input (value="1") suppresses required toggling for admins.
(function () { (() => {
var optionalFormatIds = ["1", "4", "6"]; var optionalFormatIds = ["1", "4", "6"];
function isAdminMode() { function isAdminMode() {
@@ -743,7 +743,7 @@
// Delegate change events on the format fieldset // Delegate change events on the format fieldset
var formatFieldset = document.getElementById("fieldset-formats"); var formatFieldset = document.getElementById("fieldset-formats");
if (formatFieldset) { if (formatFieldset) {
formatFieldset.addEventListener("change", function (e) { formatFieldset.addEventListener("change", (e) => {
if (e.target && e.target.name === "formats[]") { if (e.target && e.target.name === "formats[]") {
updateTfeRequired(); updateTfeRequired();
} }

View File

@@ -4,7 +4,7 @@
* Switches between integer input (pages/mo) and h/m/s time inputs (durée). * Switches between integer input (pages/mo) and h/m/s time inputs (durée).
* Updates hidden #duration_value on change. * Updates hidden #duration_value on change.
*/ */
(function () { (() => {
var unit = document.getElementById('duration_unit'); var unit = document.getElementById('duration_unit');
var hidden = document.getElementById('duration_value'); var hidden = document.getElementById('duration_value');
var intWrap = document.getElementById('duration-value-integer'); var intWrap = document.getElementById('duration-value-integer');

View File

@@ -4,9 +4,9 @@
* *
* Reads `data-admin-mode` (0/1) from the jury <fieldset>. * Reads `data-admin-mode` (0/1) from the jury <fieldset>.
*/ */
(function () { (() => {
// ── Dynamic row add/remove ────────────────────────────────────────────── // ── Dynamic row add/remove ──────────────────────────────────────────────
window.addJuryRow = function (listId, inputName, roleLabel) { window.addJuryRow = (listId, inputName, roleLabel) => {
var list = document.getElementById(listId); var list = document.getElementById(listId);
if (!list) return; if (!list) return;
var n = list.querySelectorAll('.admin-jury-entry').length + 1; var n = list.querySelectorAll('.admin-jury-entry').length + 1;
@@ -25,7 +25,7 @@
list.appendChild(div); list.appendChild(div);
}; };
window.removeJuryRow = function (btn) { window.removeJuryRow = (btn) => {
var entry = btn.closest('.admin-jury-entry'); var entry = btn.closest('.admin-jury-entry');
if (entry) entry.remove(); if (entry) entry.remove();
}; };
@@ -51,7 +51,7 @@
ulbRow.style.display = show ? '' : 'none'; ulbRow.style.display = show ? '' : 'none';
if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none'; if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none';
var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]'); var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]');
inputs.forEach(function (inp, idx) { inputs.forEach((inp, idx) => {
inp.required = adminMode ? false : show && idx === 0; inp.required = adminMode ? false : show && idx === 0;
inp.disabled = !show; inp.disabled = !show;
if (!show) inp.value = ''; if (!show) inp.value = '';

View File

@@ -4,7 +4,7 @@
* *
* Reads the container id from data-search-container-id on the pill-search div. * Reads the container id from data-search-container-id on the pill-search div.
*/ */
(function () { (() => {
var pillDiv = document.querySelector('[data-search-container-id]'); var pillDiv = document.querySelector('[data-search-container-id]');
if (!pillDiv) return; if (!pillDiv) return;
var containerId = pillDiv.getAttribute('data-search-container-id'); var containerId = pillDiv.getAttribute('data-search-container-id');

View File

@@ -7,9 +7,9 @@
* *
* Loaded on all admin pages via footer.php. * Loaded on all admin pages via footer.php.
*/ */
(function () { (() => {
// Toast accessibility — auto-focus warning toasts // Toast accessibility — auto-focus warning toasts
document.body.addEventListener('htmx:afterSettle', function (e) { document.body.addEventListener('htmx:afterSettle', (e) => {
if (e.target && e.target.id === 'toast-region') { if (e.target && e.target.id === 'toast-region') {
var warn = e.target.querySelector('.toast--warning'); var warn = e.target.querySelector('.toast--warning');
if (warn) { if (warn) {
@@ -20,7 +20,7 @@
}); });
// Markdown cheatsheet: remove stale dialogs before a new one arrives // Markdown cheatsheet: remove stale dialogs before a new one arrives
document.body.addEventListener('htmx:beforeRequest', function (e) { document.body.addEventListener('htmx:beforeRequest', (e) => {
if ( if (
e.detail.requestConfig && e.detail.requestConfig &&
e.detail.requestConfig.path === '/admin/markdown-cheatsheet-fragment.php' e.detail.requestConfig.path === '/admin/markdown-cheatsheet-fragment.php'
@@ -31,7 +31,7 @@
}); });
// Markdown cheatsheet: close on backdrop (dialog element) click // Markdown cheatsheet: close on backdrop (dialog element) click
document.body.addEventListener('click', function (e) { document.body.addEventListener('click', (e) => {
if (e.target.tagName === 'DIALOG' && e.target.id === 'md-cheatsheet-dialog') { if (e.target.tagName === 'DIALOG' && e.target.id === 'md-cheatsheet-dialog') {
e.target.close(); e.target.close();
} }

View File

@@ -4,7 +4,7 @@
* Single-open behavior on mobile (≤ 1025px). Re-initializes after HTMX swaps. * Single-open behavior on mobile (≤ 1025px). Re-initializes after HTMX swaps.
* On desktop, all panels close when crossing the breakpoint. * On desktop, all panels close when crossing the breakpoint.
*/ */
(function () { (() => {
var INDEX_SEL = '#repertoire-index'; var INDEX_SEL = '#repertoire-index';
var ACCORDION_SEL = '.rep-accordion'; var ACCORDION_SEL = '.rep-accordion';
var TOGGLE_SEL = '.rep-accordion__toggle'; var TOGGLE_SEL = '.rep-accordion__toggle';
@@ -17,18 +17,18 @@
function initAccordions(root) { function initAccordions(root) {
if (!isMobile()) return; if (!isMobile()) return;
var toggles = root.querySelectorAll(TOGGLE_SEL); var toggles = root.querySelectorAll(TOGGLE_SEL);
toggles.forEach(function (btn) { toggles.forEach((btn) => {
// Skip students column — always visible, not an accordion // Skip students column — always visible, not an accordion
if (btn.closest('[data-col="students"]')) return; if (btn.closest('[data-col="students"]')) return;
if (btn._accordionBound) return; if (btn._accordionBound) return;
btn._accordionBound = true; btn._accordionBound = true;
btn.addEventListener('click', function () { btn.addEventListener('click', () => {
var section = btn.closest(ACCORDION_SEL); var section = btn.closest(ACCORDION_SEL);
var panel = section.querySelector(PANEL_SEL); var panel = section.querySelector(PANEL_SEL);
var isOpen = btn.getAttribute('aria-expanded') === 'true'; var isOpen = btn.getAttribute('aria-expanded') === 'true';
// Close all others (except students column) // Close all others (except students column)
root.querySelectorAll(ACCORDION_SEL).forEach(function (s) { root.querySelectorAll(ACCORDION_SEL).forEach((s) => {
if (s.dataset.col === 'students') return; if (s.dataset.col === 'students') return;
var p = s.querySelector(PANEL_SEL); var p = s.querySelector(PANEL_SEL);
var t = s.querySelector(TOGGLE_SEL); var t = s.querySelector(TOGGLE_SEL);
@@ -54,7 +54,7 @@
initAccordions(document); initAccordions(document);
// Re-bind after HTMX swaps (use live DOM since e.detail.target may be detached) // Re-bind after HTMX swaps (use live DOM since e.detail.target may be detached)
document.body.addEventListener('htmx:afterSwap', function (e) { document.body.addEventListener('htmx:afterSwap', (e) => {
if ( if (
e.detail.target && e.detail.target &&
e.detail.target.matches && e.detail.target.matches &&
@@ -67,7 +67,7 @@
// Re-bind on resize crossing the breakpoint // Re-bind on resize crossing the breakpoint
var wasMobile = isMobile(); var wasMobile = isMobile();
window.addEventListener('resize', function () { window.addEventListener('resize', () => {
var nowMobile = isMobile(); var nowMobile = isMobile();
if (nowMobile !== wasMobile) { if (nowMobile !== wasMobile) {
wasMobile = nowMobile; wasMobile = nowMobile;
@@ -75,12 +75,12 @@
// Switching to desktop — close all panels // Switching to desktop — close all panels
document document
.querySelectorAll(INDEX_SEL + ' ' + TOGGLE_SEL) .querySelectorAll(INDEX_SEL + ' ' + TOGGLE_SEL)
.forEach(function (btn) { .forEach((btn) => {
btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-expanded', 'false');
}); });
document document
.querySelectorAll(INDEX_SEL + ' ' + PANEL_SEL) .querySelectorAll(INDEX_SEL + ' ' + PANEL_SEL)
.forEach(function (p) { .forEach((p) => {
p.classList.remove('is-open'); p.classList.remove('is-open');
}); });
} }

View File

@@ -4,7 +4,7 @@
* Shows a popover with HTMX-fetched student details on hover over links * Shows a popover with HTMX-fetched student details on hover over links
* with `data-student-name` attribute. * with `data-student-name` attribute.
*/ */
(function () { (() => {
var popover = document.getElementById('student-popover'); var popover = document.getElementById('student-popover');
var currentAnchor = null; var currentAnchor = null;
@@ -21,7 +21,7 @@
document.body.addEventListener( document.body.addEventListener(
'mouseenter', 'mouseenter',
function (e) { (e) => {
var a = e.target.closest('[data-student-name]'); var a = e.target.closest('[data-student-name]');
if (!a) return; if (!a) return;
currentAnchor = a; currentAnchor = a;
@@ -29,7 +29,7 @@
true true
); );
document.body.addEventListener('htmx:afterSwap', function (e) { document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target !== popover) return; if (e.detail.target !== popover) return;
if (currentAnchor) position(currentAnchor); if (currentAnchor) position(currentAnchor);
popover.hidden = false; popover.hidden = false;
@@ -37,13 +37,13 @@
document.body.addEventListener( document.body.addEventListener(
'mouseleave', 'mouseleave',
function (e) { (e) => {
if ( if (
!e.target.closest('[data-student-name]') && !e.target.closest('[data-student-name]') &&
!e.target.closest('#student-popover') !e.target.closest('#student-popover')
) )
return; return;
setTimeout(function () { setTimeout(() => {
if ( if (
!document.querySelector('[data-student-name]:hover') && !document.querySelector('[data-student-name]:hover') &&
!document.querySelector('#student-popover:hover') !document.querySelector('#student-popover:hover')

View File

@@ -3,7 +3,7 @@
* *
* Operates on #sidebar-links-form and #sidebar-link-tpl. * Operates on #sidebar-links-form and #sidebar-link-tpl.
*/ */
(function () { (() => {
var form = document.getElementById('sidebar-links-form'); var form = document.getElementById('sidebar-links-form');
var tpl = document.getElementById('sidebar-link-tpl'); var tpl = document.getElementById('sidebar-link-tpl');
if (!form || !tpl) return; if (!form || !tpl) return;
@@ -11,8 +11,8 @@
function reindexLinks() { function reindexLinks() {
var rows = form.querySelectorAll('.sidebar-link-row'); var rows = form.querySelectorAll('.sidebar-link-row');
rows.forEach(function (row, i) { rows.forEach((row, i) => {
row.querySelectorAll('input').forEach(function (inp) { row.querySelectorAll('input').forEach((inp) => {
if (inp.name) { if (inp.name) {
inp.name = inp.name.replace(/links\[\d+\]/, 'links[' + i + ']'); inp.name = inp.name.replace(/links\[\d+\]/, 'links[' + i + ']');
} }
@@ -20,14 +20,14 @@
inp.id = inp.id.replace(/sl_\d+/, 'sl_' + i); inp.id = inp.id.replace(/sl_\d+/, 'sl_' + i);
} }
}); });
row.querySelectorAll('label[for]').forEach(function (lbl) { row.querySelectorAll('label[for]').forEach((lbl) => {
lbl.setAttribute('for', lbl.getAttribute('for').replace(/sl_\d+/, 'sl_' + i)); lbl.setAttribute('for', lbl.getAttribute('for').replace(/sl_\d+/, 'sl_' + i));
}); });
}); });
} }
// Event delegation for remove buttons // Event delegation for remove buttons
form.addEventListener('click', function (e) { form.addEventListener('click', (e) => {
if (!e.target.closest('.remove-sidebar-link-btn')) return; if (!e.target.closest('.remove-sidebar-link-btn')) return;
e.preventDefault(); e.preventDefault();
e.target.closest('.sidebar-link-row').remove(); e.target.closest('.sidebar-link-row').remove();

View File

@@ -2,7 +2,7 @@
* smtp-error-focus.js — Scrolls to and focuses the SMTP field that caused a probe * smtp-error-focus.js — Scrolls to and focuses the SMTP field that caused a probe
* error. Reads the field id from data-smtp-error-field on the SMTP form. * error. Reads the field id from data-smtp-error-field on the SMTP form.
*/ */
(function () { (() => {
var form = document.querySelector('form[data-smtp-error-field]'); var form = document.querySelector('form[data-smtp-error-field]');
if (!form) return; if (!form) return;
var fieldId = form.getAttribute('data-smtp-error-field'); var fieldId = form.getAttribute('data-smtp-error-field');

View File

@@ -2,12 +2,14 @@
/** /**
* admin-toc.php — sidebar table-of-contents for long admin pages. * admin-toc.php — sidebar table-of-contents for long admin pages.
* *
* Rendered as an <aside> inside <main>, before the <article> content. * Uses the same <details class="toc"> markup as the public content pages
* Uses IntersectionObserver to highlight the active section. * (about, charte, licence). The link list is populated by admin-toc.js
* at runtime.
*/ */
?> ?>
<aside id="admin-toc" class="admin-toc" aria-label="Sur cette page"> <details class="toc" id="admin-toc" open aria-label="Sur cette page">
<nav class="admin-toc-list" id="admin-toc-list"> <summary><?= icon('caret-down', 0, 'toc-caret') ?> SUR CETTE PAGE</summary>
<!-- populated by JS (admin-toc.js, loaded via admin.min.js) --> <ul class="toc-list" id="admin-toc-list">
</nav> <!-- populated by admin-toc.js -->
</aside> </ul>
</details>

View File

@@ -36,7 +36,7 @@ function renderEntries(array $entries): string
<!-- Table of contents: collapsible on mobile, force-open on desktop --> <!-- Table of contents: collapsible on mobile, force-open on desktop -->
<details class="toc" open> <details class="toc" open>
<summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary> <summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary>
<ul> <ul class="toc-list">
<li><a href="#apropos-intro">À propos</a></li> <li><a href="#apropos-intro">À propos</a></li>
<?php if (!empty($contacts)): ?> <?php if (!empty($contacts)): ?>
<li><a href="#apropos-contacts">Contacts</a></li> <li><a href="#apropos-contacts">Contacts</a></li>
@@ -54,6 +54,7 @@ function renderEntries(array $entries): string
<?php endif; ?> <?php endif; ?>
</details> </details>
<article>
<!-- Intro text from DB --> <!-- Intro text from DB -->
<section class="content-section" id="apropos-intro"> <section class="content-section" id="apropos-intro">
<?= $aboutHtml ?> <?= $aboutHtml ?>
@@ -113,5 +114,6 @@ function renderEntries(array $entries): string
</div> </div>
</dl> </dl>
</section> </section>
</article>
</main> </main>

View File

@@ -4,7 +4,7 @@
<?php if (!empty($tocItems)): ?> <?php if (!empty($tocItems)): ?>
<details class="toc" open> <details class="toc" open>
<summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary> <summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary>
<ul> <ul class="toc-list">
<?php foreach ($tocItems as $item): ?> <?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li> <li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?> <?php endforeach; ?>
@@ -12,12 +12,12 @@
</details> </details>
<?php endif; ?> <?php endif; ?>
<div class="content"> <article>
<?php if (!empty(trim($content))): ?> <?php if (!empty(trim($content))): ?>
<?= $html ?> <?= $html ?>
<?php else: ?> <?php else: ?>
<p>Contenu à venir.</p> <p>Contenu à venir.</p>
<?php endif; ?> <?php endif; ?>
</div> </article>
</main> </main>

View File

@@ -4,7 +4,7 @@
<?php if (!empty($tocItems)): ?> <?php if (!empty($tocItems)): ?>
<details class="toc" open> <details class="toc" open>
<summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary> <summary><?= icon('caret-down', 0, 'toc-caret') ?> PARTIES</summary>
<ul> <ul class="toc-list">
<?php foreach ($tocItems as $item): ?> <?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li> <li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?> <?php endforeach; ?>
@@ -12,12 +12,12 @@
</details> </details>
<?php endif; ?> <?php endif; ?>
<div class="content"> <article>
<?php if (!empty(trim($content))): ?> <?php if (!empty(trim($content))): ?>
<?= $html ?> <?= $html ?>
<?php else: ?> <?php else: ?>
<p>Contenu à venir.</p> <p>Contenu à venir.</p>
<?php endif; ?> <?php endif; ?>
</div> </article>
</main> </main>

View File

@@ -7,7 +7,10 @@
"!app/public/assets/js/htmx.min.js", "!app/public/assets/js/htmx.min.js",
"!app/public/assets/js/overtype.min.js", "!app/public/assets/js/overtype.min.js",
"!app/public/assets/js/sortable.min.js", "!app/public/assets/js/sortable.min.js",
"!app/public/assets/js/vendor/**" "!app/public/assets/js/vendor/**",
"!app/public/assets/css/filepond*.css",
"!app/public/assets/css/modern-normalize*.css",
"!app/public/assets/dist/**"
] ]
}, },
"css": { "css": {
@@ -15,7 +18,7 @@
"enabled": true "enabled": true
}, },
"linter": { "linter": {
"enabled": false "enabled": true
} }
}, },
"linter": { "linter": {

View File

@@ -74,6 +74,10 @@ build-js:
build-install: build-install:
@npm ci @npm ci
[group('build')]
build-lint:
@npx biome lint app/public/assets/css/ app/public/assets/js/app/ scripts/
[group('build')] [group('build')]
build-check: build-check:
@echo "Checking if build output is up to date…" @echo "Checking if build output is up to date…"

View File

@@ -37,6 +37,10 @@ if (!existsSync(resolve(root, "node_modules"))) {
execSync("npm ci", { cwd: root, stdio: "inherit" }); execSync("npm ci", { cwd: root, stdio: "inherit" });
} }
if (buildAll || onlyCss) {
run("Linting CSS + JS (biome)", "npx biome lint app/public/assets/css/ app/public/assets/js/app/ scripts/ || true");
}
if (buildAll || onlyCss) { if (buildAll || onlyCss) {
run("Building CSS bundles", "node scripts/build-css.mjs"); run("Building CSS bundles", "node scripts/build-css.mjs");
} }