Refactor apropos/charte/licence pages: shared layout, TOC anchors, and UI polish

Unify the three public pages (à propos, charte, licence) onto a single
grid layout (.page-content) with sticky TOC sidebar, replacing the old
separate  /  /  markup.

- Merge about.php, charte.php, licence.php templates into shared
  .page-content / .content-section structure
- Add CommonMark HeadingPermalinkExtension for stable heading anchors
- Use SlugNormalizer for TOC links so they match rendered heading IDs
- Standardize link styling across content blocks: bold black, accent on
  hover (consistent with global link style)
- Fix code block wrapping: use pre-wrap instead of pre, constrain grid
  columns with min-width:0, auto scrollbar
- Fix apropos page grid placement: force content-section into column 2
  so contacts and credits stay in the content area, not the sidebar

Also includes accumulated WIP changes:
- Header gradient: hardcoded purple-to-green (replaces CSS variables)
- Search placeholder font
- Duration field: replace minutes/sec/heures with h:m:s time inputs
- TFE file optional for formats 1,4,6 with client-side JS toggle
- Licence form: em-dash to hyphen, details/summary classes
- Pill search: block Enter key form submission when no results
- Draft autosave: remove CSRF rotation (broke concurrent FilePond uploads)
- Language pill: clear hints for excluded main languages
- Search results: gradient placeholder cards for items without covers
- TFE display: format durée values as XhYm instead of decimal
This commit is contained in:
Pontoporeia
2026-06-15 16:35:17 +02:00
parent 928e074d24
commit 19bf9f101a
27 changed files with 636 additions and 342 deletions

View File

@@ -1,28 +1,22 @@
/* ============================================================
À PROPOS PAGE (apropos.php)
Root class: .apropos-main
À PROPOS / CHARTE / LICENCE pages
Root class: .page-content
============================================================ */
/* ------------------------------------------------------------------ */
/* Page shell */
/* ------------------------------------------------------------------ */
.apropos-main {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--space-xl) var(--space-l) var(--space-2xl);
/* Override body overflow:hidden — these pages use the viewport scrollbar
so that anchor navigation (#fragment) works natively. */
.apropos-body {
overflow: auto;
}
/* ------------------------------------------------------------------ */
/* Two-column layout: sticky TOC nav | content */
/* ------------------------------------------------------------------ */
.apropos-layout {
.page-content {
flex: 1;
min-height: 0;
scroll-behavior: smooth;
padding: var(--space-xl) var(--space-l) var(--space-2xl);
display: grid;
grid-template-columns: 180px 1fr;
gap: var(--space-2xl);
width: 100%;
align-items: start;
}
@@ -36,13 +30,13 @@
}
.apropos-toc-label {
font-family: var(--font-body);
font-family: var(--font-display);
font-size: var(--step--2);
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-tertiary);
font-weight: 400;
color: var(--text-primary);
margin: 0 0 var(--space-2xs) 0;
padding-bottom: var(--space-2xs);
border-bottom: 1px solid var(--text-primary);
}
.apropos-toc ul {
@@ -62,13 +56,10 @@
display: block;
padding: var(--space-3xs) 0;
transition: color 0.15s;
border-left: 2px solid transparent;
padding-left: var(--space-2xs);
}
.apropos-toc ul a:hover {
color: var(--accent-primary);
border-left-color: var(--accent-primary);
}
.apropos-toc-link:first-of-type {
@@ -96,19 +87,115 @@
/* Right — main content area */
/* ------------------------------------------------------------------ */
.apropos-content {
display: flex;
flex-direction: column;
gap: 0;
.page-content > .content,
.page-content > .content-section {
grid-column: 2;
min-width: 0;
max-width: 100%;
}
.apropos-section {
/* Shared typography for about-page sections and charte/licence content */
.content,
.content-section {
display: block;
max-width: 100%;
font-family: var(--font-body);
font-size: var(--step-0);
line-height: 1.6;
color: var(--text-primary);
font-weight: 300;
}
.content *,
.content-section * {
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.content p,
.content-section p {
margin: 0 0 1em 0;
}
.content p:last-child,
.content-section p:last-child {
margin-bottom: 0;
}
.content :where(h1, h2, h3),
.content-section :where(h1, h2, h3) {
margin: 1.5em 0 0.5em 0;
}
.content a,
.content-section a {
color: inherit;
text-decoration: none;
font-weight: 700;
}
.content a:hover,
.content-section a:hover {
color: var(--accent-primary);
text-decoration: none;
}
.content ul,
.content ol,
.content-section ul,
.content-section ol {
padding-left: var(--space-m);
margin-bottom: var(--space-s);
}
.content li,
.content-section li {
margin-bottom: 0.3em;
}
.content strong,
.content-section strong {
font-weight: 700;
}
.content em,
.content-section em {
font-style: italic;
}
.content :where(pre, pre code, code),
.content-section :where(pre, pre code, code) {
display: block;
max-width: 100%;
overflow-x: auto;
overflow-wrap: normal;
word-break: normal;
font-family: "Courier New", Courier, monospace;
font-size: 0.88em;
background: var(--bg-tertiary);
padding: 0.5em 0.75em;
border-radius: var(--radius);
white-space: pre-wrap;
word-wrap: break-word;
}
#apropos-intro *,
#apropos-contacts *,
#apropos-credits * {
overflow-wrap: anywhere;
word-break: break-word;
max-width: 100%;
}
/* Section separators (about page only — .content-section adds dividers) */
.page-content > .content-section {
padding-bottom: var(--space-xl);
border-bottom: 1px solid var(--border-primary);
margin-bottom: var(--space-xl);
}
.apropos-section:last-child {
.page-content > .content-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
@@ -118,7 +205,7 @@
/* Section titles */
/* ------------------------------------------------------------------ */
.apropos-section-title {
.content-section-title {
font-family: var(--font-display);
font-size: var(--step-3);
font-weight: 400;
@@ -127,59 +214,9 @@
line-height: 1.1;
}
/* ------------------------------------------------------------------ */
/* Intro prose — Markdown-rendered content */
/* ------------------------------------------------------------------ */
.prose {
font-family: var(--font-body);
font-size: var(--step-0);
line-height: 1.6;
color: var(--text-primary);
font-weight: 400;
}
.prose p {
margin: 0 0 1em 0;
}
.prose p:last-child {
margin-bottom: 0;
}
.prose :where(h1, h2, h3) {
margin: 1.5em 0 0.5em 0;
}
.prose a {
color: var(--accent-primary);
text-decoration: underline;
text-underline-offset: 2px;
}
.prose ul,
.prose ol {
padding-left: var(--space-m);
margin-bottom: var(--space-s);
}
.prose li {
margin-bottom: 0.3em;
}
.prose strong {
font-weight: 700;
}
.prose em {
font-style: italic;
}
.prose code {
font-family: "Courier New", Courier, monospace;
font-size: 0.88em;
background: var(--bg-tertiary);
padding: 0.1em 0.3em;
border-radius: var(--radius);
/* Hide CommonMark heading permalink anchors (needed only for their id attr) */
.heading-permalink {
display: none;
}
/* ------------------------------------------------------------------ */
@@ -220,15 +257,15 @@
.apropos-contact-card a {
font-size: var(--step--1);
color: var(--accent-primary);
text-decoration: underline;
text-underline-offset: 2px;
transition: opacity 0.15s;
color: inherit;
text-decoration: none;
font-weight: 700;
transition: color 0.15s;
}
.apropos-contact-card a:hover {
color: var(--accent-primary);
opacity: 1;
text-decoration: none;
}
/* ------------------------------------------------------------------ */
@@ -284,7 +321,7 @@
/* ------------------------------------------------------------------ */
@media (max-width: 900px) {
.apropos-layout {
.page-content {
grid-template-columns: 1fr;
gap: var(--space-l);
}
@@ -309,32 +346,27 @@
gap: var(--space-xs);
}
.apropos-toc ul a {
border-left: none;
padding-left: 0;
}
.apropos-toc-link {
border-top: none;
padding-top: 0;
margin-left: auto;
}
.prose {
.content-section {
font-size: var(--step-0);
}
.apropos-section-title {
.content-section-title {
font-size: var(--step-2);
}
}
@media (max-width: 600px) {
.apropos-main {
.page-content {
padding: var(--space-m) var(--space-s) var(--space-xl);
}
.prose {
.content-section {
font-size: var(--step-0);
}

View File

@@ -38,10 +38,10 @@
--accent-red: #f25a5a;
/* Gradient (header) */
--gradient-1: #3c856c;
--gradient-2: #60ecb4;
--gradient-3: #e390ff;
--gradient-4: #9557b5;
--gradient-1: #42963f;
--gradient-2: #65e478;
--gradient-3: #57abc7;
--gradient-4: #db53ed;
/* Header decorative */
--header-gradient-fade: rgba(149, 87, 181, 0);

View File

@@ -8,12 +8,13 @@
header {
vertical-align: center;
flex-shrink: 0;
background: #9557B5;
background: linear-gradient(
180deg,
var(--gradient-1) 0%,
var(--gradient-2) 33%,
var(--gradient-3) 66%,
var(--gradient-4) 100%
0deg,
rgba(149, 87, 181, 1) 0%,
rgba(192, 93, 225, 1) 25%,
rgba(51, 191, 135, 1) 75%,
rgba(60, 133, 108, 1) 100%
);
}
@@ -60,7 +61,6 @@ header nav ul a[aria-current="page"] {
padding-bottom: 1px;
}
/* ── Logo ───────────────────────────────────────────────────────────── */
.nav-logo {
@@ -130,8 +130,12 @@ header nav ul a[aria-current="page"] {
transition: all 0.3s ease-out;
}
.navicon::before { top: -7px; }
.navicon::after { bottom: -7px; }
.navicon::before {
top: -7px;
}
.navicon::after {
bottom: -7px;
}
/* ── Mobile (≤ 640px) ───────────────────────────────────────────────── */
@@ -160,11 +164,14 @@ header nav ul a[aria-current="page"] {
}
header nav[aria-label="Navigation principale"]
.nav-left-links li:not(:first-child) {
.nav-left-links
li:not(:first-child) {
display: none;
}
.menu-icon { display: flex; }
.menu-icon {
display: flex;
}
header nav[aria-label="Navigation principale"] .nav-mobile-links {
display: block;

View File

@@ -6,7 +6,7 @@
.header-search-wrap {
padding: 0;
flex-shrink: 0;
background: linear-gradient(180deg, var(--gradient-4) 0%, #ffffffee 100%);
background: linear-gradient(180deg, #9557B5 0%, #ffffffee 100%);
}
.header-search-form { width: 100%; }
@@ -40,4 +40,6 @@
.header-search-input-wrap input::placeholder {
color: var(--accent-primary) !important;
font-family: var(--font-body);
font-weight: 300;
}

View File

@@ -352,6 +352,34 @@
border-radius: var(--radius);
}
.duration-time-inputs {
display: flex;
align-items: flex-end;
gap: var(--space-xs);
}
.duration-time-fields {
display: inline-flex;
align-items: center;
gap: var(--space-3xs);
}
.duration-time-fields span {
font-size: var(--step--1);
color: var(--text-secondary);
}
.licence-details {
display: inline-flex;
}
.licence-summary {
display: inline-flex;
align-items: center;
gap: var(--space-3xs);
cursor: pointer;
}
.licence-degree h4 {
margin: 0 0 var(--space-2xs);
font-weight: 600;

View File

@@ -111,8 +111,8 @@
background: linear-gradient(
180deg,
rgba(60, 133, 108, 1) 0%,
rgba(96, 236, 180, 1) 33%,
rgba(227, 144, 255, 1) 66%,
rgba(51, 191, 135, 1) 25%,
rgba(192, 93, 225, 1) 75%,
rgba(149, 87, 181, 1) 100%
);
}

View File

@@ -223,6 +223,56 @@
gap: var(--space-3xs);
}
.result-card__cover {
margin: 0;
}
.result-card__cover img {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
display: block;
border-radius: 7px 7px 0 0;
}
.result-card__gradient {
width: 100%;
aspect-ratio: 4/3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-s);
text-align: center;
box-sizing: border-box;
border-radius: 7px 7px 0 0;
background: linear-gradient(
180deg,
rgba(60, 133, 108, 1) 0%,
rgba(51, 191, 135, 1) 25%,
rgba(192, 93, 225, 1) 75%,
rgba(149, 87, 181, 1) 100%
);
}
.result-card__gradient-author {
color: var(--accent-foreground);
font-size: var(--step--2);
opacity: 0.85;
margin-bottom: 0.25rem;
display: block;
}
.result-card__gradient-title {
color: var(--accent-foreground);
font-size: var(--step--1);
font-weight: 600;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.result-card__authors {
font-size: var(--step--1);
font-weight: 500;
@@ -391,8 +441,8 @@
background: linear-gradient(
180deg,
rgba(60, 133, 108, 1) 0%,
rgba(96, 236, 180, 1) 33%,
rgba(227, 144, 255, 1) 66%,
rgba(51, 191, 135, 1) 25%,
rgba(192, 93, 225, 1) 75%,
rgba(149, 87, 181, 1) 100%
);
display: flex;

View File

@@ -643,6 +643,7 @@
enableFilepondMode();
_xamxamFilepondReady = false;
window.XamxamInitFilePonds();
if (window.XamxamUpdateTfeRequired) window.XamxamUpdateTfeRequired();
setTimeout(() => {
_xamxamFilepondReady = true;
}, 0);
@@ -694,6 +695,68 @@
}
});
// ── TFE file optional when Site web (1), Performance (4) or Installation (6) ──
// 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.
// admin_mode hidden input (value="1") suppresses required toggling for admins.
(function () {
var optionalFormatIds = ["1", "4", "6"];
function isAdminMode() {
var el = document.querySelector('input[name="admin_mode"]');
return el && el.value === "1";
}
function updateTfeRequired() {
if (isAdminMode()) return;
var tfeInput = document.getElementById("tfe-files-input");
if (!tfeInput) return;
var checkedAny = false;
var boxes = document.querySelectorAll('input[name="formats[]"]:checked');
for (var i = 0; i < boxes.length; i++) {
if (optionalFormatIds.indexOf(boxes[i].value) !== -1) {
checkedAny = true;
break;
}
}
// Find the label for the TFE input (its parent group's <label>)
var fieldGroup = tfeInput.closest(".admin-files-fieldgroup");
var label = fieldGroup ? fieldGroup.querySelector("label[for='tfe-files-input']") : null;
if (checkedAny) {
tfeInput.removeAttribute("required");
// Replace asterisk + optional text
if (label) {
label.textContent = "TFE (optionnel pour ce format)";
}
} else {
tfeInput.setAttribute("required", "");
if (label) {
label.innerHTML = "TFE <span class='asterisk'>*</span>";
}
}
}
// Delegate change events on the format fieldset
var formatFieldset = document.getElementById("fieldset-formats");
if (formatFieldset) {
formatFieldset.addEventListener("change", function (e) {
if (e.target && e.target.name === "formats[]") {
updateTfeRequired();
}
});
}
// Run once on page load
updateTfeRequired();
// Expose for HTMX afterSwap re-init
window.XamxamUpdateTfeRequired = updateTfeRequired;
})();
// ── Relink file browser ──────────────────────────────────────────
/**

View File

@@ -159,8 +159,14 @@
}
highlight(selectedIdx);
} else if (e.key === "Enter") {
// Always prevent Enter from submitting the form.
// If there are no suggestions (e.g., "anglais" in language
// search — excluded main language), the Enter key would
// otherwise propagate to the form and trigger its hx-post to
// draft.php, causing the JSON response to replace the form
// content.
e.preventDefault();
if (items.length > 0) {
e.preventDefault();
if (selectedIdx >= 0 && selectedIdx < items.length) {
selectPill(items[selectedIdx]);
} else {