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

@@ -37,8 +37,8 @@ $adminMode = ($_POST['admin_mode'] ?? '0') === '1';
$editMode = ($_POST['edit_mode'] ?? '0') === '1';
$errorFieldName = $errorFieldName ?? null;
// TFE file is optional when format is Site web (3), Performance (4) or Installation (6)
$noTfeFileFormats = [3, 4, 6];
// TFE file is optional when format is Site web (1), Performance (4) or Installation (6)
$noTfeFileFormats = [1, 4, 6];
$tfeFileOptional = !empty(array_intersect($selectedFormats, $noTfeFileFormats));
$websiteUrl = htmlspecialchars($_POST['website_url'] ?? '');

View File

@@ -1,6 +1,6 @@
<?php
/**
* Shared partial "Degrés d'ouverture et licences" fieldset.
* Shared partial - "Degrés d'ouverture et licences" fieldset.
*
* Renders:
* 1. Généralités (editable via form help blocks)
@@ -9,13 +9,13 @@
* 4. CC2r checkbox
*
* Variables consumed:
* array $formData raw form data for repopulation
* array $licenseTypes [{id, name}]
* bool $libreEnabled show Libre option (always true for admin)
* bool $interneEnabled show Interne option
* bool $interditEnabled show Interdit option
* string $generalitiesHtml HTML content for Généralités section (editable)
* int $defaultAccessTypeId default selected access type (default: 2)
* array $formData - raw form data for repopulation
* array $licenseTypes - [{id, name}]
* bool $libreEnabled - show Libre option (always true for admin)
* bool $interneEnabled - show Interne option
* bool $interditEnabled - show Interdit option
* string $generalitiesHtml - HTML content for Généralités section (editable)
* int $defaultAccessTypeId - default selected access type (default: 2)
*/
$formData = $formData ?? [];
@@ -34,7 +34,7 @@ $adminMode = $adminMode ?? false;
<div class="licence-choice">
<p class="licence-prompt">J'autorise l'erg à archiver mon TFE de la manière suivante :</p>
<?php
// access_type_id may be null (meaning "not set"). Keep null to select "" radio.
// access_type_id may be null (meaning "not set"). Keep null to select "-" radio.
$selectedAccess = array_key_exists('access_type_id', $formData) ? $formData['access_type_id'] : $defaultAccessTypeId;
?>
@@ -47,7 +47,7 @@ $adminMode = $adminMode ?? false;
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '' || $selectedAccess === null ? 'checked' : '' ?>>
<strong></strong> Non défini
<strong>-</strong> Non défini
</label>
</div>
<?php endif; ?>
@@ -64,10 +64,10 @@ $adminMode = $adminMode ?? false;
<strong>🔓 Libre</strong>
<br>
</label>
<details>
<summary> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<details class="licence-details">
<summary class="licence-summary"> <svg xmlns="http://www.w3.org/2000/svg" width="1rem" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<p>
Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de lerg. Je suis conscient des responsabilités et obligations légales qui viennent avec une diffusion externe et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam.
Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg. Je suis conscient des responsabilités et obligations légales qui viennent avec une diffusion externe et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam.
</p>
</details>
</div>
@@ -85,10 +85,10 @@ $adminMode = $adminMode ?? false;
<strong>🔒 Interne</strong>
</label>
<br>
<details>
<summary> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<details class="licence-details">
<summary class="licence-summary"> <svg xmlns="http://www.w3.org/2000/svg" width="1rem" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<p>
Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. Jautorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg.
Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. J'autorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg.
</p>
</details>
@@ -108,10 +108,10 @@ $adminMode = $adminMode ?? false;
</label>
<br>
<details>
<summary> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<details class="licence-details">
<summary class="licence-summary"> <svg xmlns="http://www.w3.org/2000/svg" width="1rem" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path></svg> Info</summary>
<p>
Mon TFE nest pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
</p>
</details>
</div>
@@ -121,14 +121,14 @@ $adminMode = $adminMode ?? false;
<!-- Seed saved licence values for the initial htmx load.
These are overridden by visible inputs inside .licence-license-choice
once htmx renders them later DOM order wins in POST. -->
once htmx renders them - later DOM order wins in POST. -->
<input type="hidden" name="license_id" value="<?= htmlspecialchars((string)($formData['license_id'] ?? '')) ?>">
<input type="hidden" name="license_custom" value="<?= htmlspecialchars($formData['license_custom'] ?? '') ?>">
<input type="hidden" name="cc2r" value="<?= !empty($formData['cc2r']) ? '1' : '' ?>">
<?php $wantLicense = !empty($formData['license_id']) || !empty($formData['license_custom']); ?>
<input type="hidden" name="want_license" value="<?= $wantLicense ? '1' : '' ?>">
<!-- Licence swapped via htmx when radio changes -->
<!-- Licence - swapped via htmx when radio changes -->
<div class="licence-license-choice"
hx-post="<?= $adminMode ? '/admin/fragments/licence.php' : '/partage/fragments/licence.php' ?>"
hx-trigger="load"

View File

@@ -418,37 +418,71 @@ if ($filesMode === 'add'): ?>
<?php endif; ?>
<!-- ═══════════════════ Durée ═══════════════════ -->
<fieldset>
<?php
$_durRaw = $durationValue ?? ($formData['duration_value'] ?? null);
$_durUnit = $durationUnit ?? ($formData['duration_unit'] ?? 'pages');
$_durFloat = $_durRaw !== null && $_durRaw !== '' ? (float)$_durRaw : null;
// Pre-split stored hours into h/m/s for the time-input fields
$_durH = '';
$_durM = '';
$_durS = '';
if ($_durFloat !== null && $_durUnit === 'durée') {
$_durH = (int)floor($_durFloat);
$_remaining = round(($_durFloat - $_durH) * 3600);
$_durM = (int)floor($_remaining / 60);
$_durS = $_remaining % 60;
$_durH = (string)$_durH;
$_durM = (string)$_durM;
$_durS = (string)$_durS;
}
?>
<fieldset id="duration-fieldset">
<legend>Durée</legend>
<div class="admin-form-group admin-form-group--inline">
<div class="admin-form-group">
<div>
<label for="duration_unit">Unité :</label>
<select id="duration_unit" name="duration_unit">
<?php
$_currentUnit = $durationUnit ?? ($formData['duration_unit'] ?? 'pages');
$_units = [
'pages' => 'pages',
'minutes' => 'minutes',
'sec' => 'secondes',
'heures' => 'heures',
'mo' => 'Mo',
'durée' => 'durée (h:m:s)',
];
foreach ($_units as $_val => $_label): ?>
<option value="<?= $_val ?>" <?= $_currentUnit === $_val ? 'selected' : '' ?>><?= htmlspecialchars($_label) ?></option>
<?php endforeach; unset($_units, $_currentUnit, $_val, $_label); ?>
<option value="<?= $_val ?>" <?= $_durUnit === $_val ? 'selected' : '' ?>><?= htmlspecialchars($_label) ?></option>
<?php endforeach; unset($_units, $_val, $_label); ?>
</select>
</div>
<div>
<label for="duration_value">Valeur :</label>
<input type="number" id="duration_value" name="duration_value"
value="<?= htmlspecialchars((string)($durationValue ?? ($formData['duration_value'] ?? ''))) ?>"
step="0.1" min="0" placeholder="0"
<!-- Integer input for pages / Mo -->
<div id="duration-value-integer"<?= $_durUnit === 'durée' ? ' style="display:none"' : '' ?>>
<label for="duration_value_int">Valeur :</label>
<input type="number" id="duration_value_int"
value="<?= htmlspecialchars($_durUnit !== 'durée' ? (string)($_durFloat ?? '') : '') ?>"
step="1" min="0" placeholder="0"
style="width: 8ch;">
</div>
<!-- Time inputs for durée -->
<div id="duration-value-time" class="duration-time-inputs"<?= $_durUnit !== 'durée' ? ' style="display:none"' : '' ?>>
<label>Durée :</label>
<span class="duration-time-fields">
<input type="number" id="duration_h" value="<?= htmlspecialchars($_durH) ?>"
step="1" min="0" placeholder="0" style="width: 5ch;" aria-label="Heures">
<span>h</span>
<input type="number" id="duration_m" value="<?= htmlspecialchars($_durM) ?>"
step="1" min="0" max="59" placeholder="0" style="width: 5ch;" aria-label="Minutes">
<span>m</span>
<input type="number" id="duration_s" value="<?= htmlspecialchars($_durS) ?>"
step="1" min="0" max="59" placeholder="0" style="width: 5ch;" aria-label="Secondes">
<span>s</span>
</span>
</div>
</div>
<small>Optionnel. Exemples : 88 pages, 32 minutes, 1.5 heures, 120 Mo.</small>
</div>
<!-- Hidden field: always submitted, populated by JS on unit change / submit -->
<input type="hidden" id="duration_value" name="duration_value"
value="<?= htmlspecialchars((string)($_durFloat ?? '')) ?>">
<small>Optionnel. Exemples : 88 pages, 120 Mo, 1h30.</small>
</fieldset>
<?php unset($_durRaw, $_durUnit, $_durFloat, $_durH, $_durM, $_durS, $_remaining); ?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
@@ -607,3 +641,47 @@ if ($filesMode === 'add'): ?>
</div>
</form>
<script>
(function() {
var unit = document.getElementById('duration_unit');
var hidden = document.getElementById('duration_value');
var intWrap = document.getElementById('duration-value-integer');
var intInput = document.getElementById('duration_value_int');
var timeWrap = document.getElementById('duration-value-time');
var hInput = document.getElementById('duration_h');
var mInput = document.getElementById('duration_m');
var sInput = document.getElementById('duration_s');
if (!unit || !hidden) return;
function updateHidden() {
if (unit.value === 'durée') {
var h = parseInt(hInput.value, 10) || 0;
var m = parseInt(mInput.value, 10) || 0;
var s = parseInt(sInput.value, 10) || 0;
var total = h + m / 60 + s / 3600;
hidden.value = total > 0 ? total.toFixed(6) : '';
} else {
hidden.value = intInput.value;
}
}
function toggleFields() {
if (unit.value === 'durée') {
intWrap.style.display = 'none';
timeWrap.style.display = '';
} else {
timeWrap.style.display = 'none';
intWrap.style.display = '';
}
updateHidden();
}
unit.addEventListener('change', toggleFields);
if (intInput) intInput.addEventListener('input', updateHidden);
if (hInput) hInput.addEventListener('input', updateHidden);
if (mInput) mInput.addEventListener('input', updateHidden);
if (sInput) sInput.addEventListener('input', updateHidden);
toggleFields();
})();
</script>

View File

@@ -31,97 +31,87 @@ function renderEntries(array $entries): string
$suffix = implode(" & ", array_slice($parts, -2));
return $prefix !== "" ? $prefix . ", " . $suffix : $suffix;
} ?>
<main class="apropos-main" id="main-content">
<div class="apropos-layout">
<!-- LEFT: sticky table of contents -->
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">Parties</p>
<ul>
<li><a href="#apropos-intro">À propos</a></li>
<?php if (!empty($contacts)): ?>
<li><a href="#apropos-contacts">Contacts</a></li>
<?php endif; ?>
<li><a href="#apropos-credits">Crédits</a></li>
</ul>
<?php if (!empty($sidebarLinks)): ?>
<?php foreach ($sidebarLinks as $sl): ?>
<div class="apropos-toc-link">
<a href="<?= htmlspecialchars($sl['url'] ?? '#') ?>" target="_blank" rel="noopener">
<?= htmlspecialchars($sl['label'] ?? 'Lien') ?> ↗
</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<!-- MIDDLE: main prose + sections -->
<div class="apropos-content">
<!-- Intro text from DB -->
<section class="apropos-section" id="apropos-intro">
<div class="prose">
<?= $aboutHtml ?>
</div>
</section>
<main class="page-content" id="main-content">
<!-- LEFT: sticky table of contents -->
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">PARTIES</p>
<ul>
<li><a href="#apropos-intro">À propos</a></li>
<?php if (!empty($contacts)): ?>
<!-- Contacts section -->
<section class="apropos-section" id="apropos-contacts">
<h2 class="apropos-section-title">Contacts</h2>
<div class="apropos-contacts-grid">
<?php foreach ($contacts as $group): ?>
<address class="apropos-contact-card">
<?= renderEntries($group["entries"] ?? []) ?>
<?php if (!empty($group["role"])): ?>
<span><?= htmlspecialchars($group["role"]) ?></span>
<?php endif; ?>
<?php
$emails = array_filter(
array_column($group["entries"] ?? [], "email"),
fn($e) => !empty($e),
);
foreach ($emails as $email): ?>
<a href="<?= EmailObfuscator::mailto($email) ?>"><?= EmailObfuscator::email($email) ?></a>
<?php endforeach;
?>
</address>
<?php endforeach; ?>
</div>
</section>
<li><a href="#apropos-contacts">Contacts</a></li>
<?php endif; ?>
<!-- Credits section (hardcoded) -->
<section class="apropos-section" id="apropos-credits">
<h2 class="apropos-section-title">Crédits</h2>
<dl class="apropos-credits-list">
<div class="apropos-credit-row">
<dt>Design & développement</dt>
<dd>
<a class="apropos-entry" target="_blank" href='&#x6D;&#x61;&#x69;&#x6C;&#x74;&#x6F;&#x3A;&#x6F;&#x6C;&#x69;&#x39;&#x38;&#x6D;&#x61;&#x72;&#x6C;&#x79;&#x40;&#x67;&#x6D;&#x61;&#x69;&#x6C;&#x2E;&#x63;&#x6F;&#x6D;&#xA;'>Olivia Marly</a>,
<a class="apropos-entry" target="_blank" href='https://tgm.happyngreen.fr'>Théophile Gerveau-Mercier</a> &
<a class="apropos-entry" target="_blank" href='https://theohennequin.com'>Théo Hennequin</a>
</dd>
</div>
<div class="apropos-credit-row">
<dt>Typographies</dt>
<dd>
<a class="apropos-entry" target="_blank" href='https://typotheque.genderfluid.space/fr/fontes/ductus'><b>Ductus</b> - Amélie Dumont</a> &
<a class="apropos-entry" target="_blank" href='https://typotheque.genderfluid.space/fr/fontes/bbb-dm-sans'><b>BBB DM Sans</b> - Camille Circlude, Eugénie Bidaut, Mariel Nils, Bérénice Bouin</a>
</dd>
</div>
<div class="apropos-credit-row">
<dt>Iconographie</dt>
<dd>
<a class="apropos-entry" target="_blank" href="https://phosphoricons.com/">Phosphor Icons</a> —
<a class="apropos-entry" target="_blank" href="https://mit-license.org/">MIT</a>,
par Helena Zhang et Tobias Fried
</dd>
</div>
</dl>
</section>
<li><a href="#apropos-credits">Crédits</a></li>
</ul>
<?php if (!empty($sidebarLinks)): ?>
<?php foreach ($sidebarLinks as $sl): ?>
<div class="apropos-toc-link">
<a href="<?= htmlspecialchars($sl['url'] ?? '#') ?>" target="_blank" rel="noopener">
<?= htmlspecialchars($sl['label'] ?? 'Lien') ?> ↗
</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<!-- Intro text from DB -->
<section class="content-section" id="apropos-intro">
<?= $aboutHtml ?>
</section>
<?php if (!empty($contacts)): ?>
<section class="content-section" id="apropos-contacts">
<h2 class="content-section-title">Contacts</h2>
<div class="apropos-contacts-grid">
<?php foreach ($contacts as $group): ?>
<address class="apropos-contact-card">
<?= renderEntries($group["entries"] ?? []) ?>
<?php if (!empty($group["role"])): ?>
<span><?= htmlspecialchars($group["role"]) ?></span>
<?php endif; ?>
<?php
$emails = array_filter(
array_column($group["entries"] ?? [], "email"),
fn($e) => !empty($e),
);
foreach ($emails as $email): ?>
<a href="<?= EmailObfuscator::mailto($email) ?>"><?= EmailObfuscator::email($email) ?></a>
<?php endforeach;
?>
</address>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<!-- Credits section (hardcoded) -->
<section class="content-section" id="apropos-credits">
<h2 class="content-section-title">Crédits</h2>
<dl class="apropos-credits-list">
<div class="apropos-credit-row">
<dt>Design & développement</dt>
<dd>
<a class="apropos-entry" target="_blank" href='&#x6D;&#x61;&#x69;&#x6C;&#x74;&#x6F;&#x3A;&#x6F;&#x6C;&#x69;&#x39;&#x38;&#x6D;&#x61;&#x72;&#x6C;&#x79;&#x40;&#x67;&#x6D;&#x61;&#x69;&#x6C;&#x2E;&#x63;&#x6F;&#x6D;&#xA;'>Olivia Marly</a>,
<a class="apropos-entry" target="_blank" href='https://tgm.happyngreen.fr'>Théophile Gerveau-Mercier</a> &
<a class="apropos-entry" target="_blank" href='https://theohennequin.com'>Théo Hennequin</a>
</dd>
</div>
<div class="apropos-credit-row">
<dt>Typographies</dt>
<dd>
<a class="apropos-entry" target="_blank" href='https://typotheque.genderfluid.space/fr/fontes/ductus'><b>Ductus</b> - Amélie Dumont</a> &
<a class="apropos-entry" target="_blank" href='https://typotheque.genderfluid.space/fr/fontes/bbb-dm-sans'><b>BBB DM Sans</b> - Camille Circlude, Eugénie Bidaut, Mariel Nils, Bérénice Bouin</a>
</dd>
</div>
<div class="apropos-credit-row">
<dt>Iconographie</dt>
<dd>
<a class="apropos-entry" target="_blank" href="https://phosphoricons.com/">Phosphor Icons</a> —
<a class="apropos-entry" target="_blank" href="https://mit-license.org/">MIT</a>,
par Helena Zhang et Tobias Fried
</dd>
</div>
</dl>
</section>
</div>
</main>

View File

@@ -1,30 +1,23 @@
<main class="apropos-main" id="main-content">
<div class="apropos-layout">
<main class="page-content" id="main-content">
<!-- LEFT: sticky table of contents -->
<?php if (!empty($tocItems)): ?>
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">Parties</p>
<ul>
<?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
<!-- LEFT: sticky table of contents -->
<?php if (!empty($tocItems)): ?>
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">PARTIES</p>
<ul>
<?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
<?php endif; ?>
<div class="content">
<?php if (!empty(trim($content))): ?>
<?= $html ?>
<?php else: ?>
<p>Contenu à venir.</p>
<?php endif; ?>
<!-- MIDDLE: main prose -->
<div class="apropos-content">
<section class="apropos-section">
<div class="prose">
<?php if (!empty(trim($content))): ?>
<?= $html ?>
<?php else: ?>
<p>Contenu à venir.</p>
<?php endif; ?>
</div>
</section>
</div>
</div>
</main>

View File

@@ -1,30 +1,23 @@
<main class="apropos-main" id="main-content">
<div class="apropos-layout">
<main class="page-content" id="main-content">
<!-- LEFT: sticky table of contents -->
<?php if (!empty($tocItems)): ?>
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">Parties</p>
<ul>
<?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
<!-- LEFT: sticky table of contents -->
<?php if (!empty($tocItems)): ?>
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">PARTIES</p>
<ul>
<?php foreach ($tocItems as $item): ?>
<li><a href="<?= htmlspecialchars($item['href']) ?>"><?= htmlspecialchars($item['label']) ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
<?php endif; ?>
<div class="content">
<?php if (!empty(trim($content))): ?>
<?= $html ?>
<?php else: ?>
<p>Contenu à venir.</p>
<?php endif; ?>
<!-- MIDDLE: main prose -->
<div class="apropos-content">
<section class="apropos-section">
<div class="prose">
<?php if (!empty(trim($content))): ?>
<?= $html ?>
<?php else: ?>
<p>Contenu à venir.</p>
<?php endif; ?>
</div>
</section>
</div>
</div>
</main>

View File

@@ -76,15 +76,22 @@
<ul class="results-grid">
<?php foreach ($results as $item): ?>
<?php $thumb = $coverMap[$item['id']] ?? null; ?>
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card<?= $thumb ? ' result-card--has-cover' : '' ?>">
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card">
<?php if ($thumb): ?>
<figure class="result-card__cover">
<img src="/media?path=<?= urlencode($thumb) ?>"
alt="Couverture — <?= htmlspecialchars($item['title']) ?>"
loading="lazy">
</figure>
<?php else: ?>
<div class="result-card__gradient">
<span class="result-card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="result-card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
</div>
<?php endif; ?>
<?php if ($thumb): ?>
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<?php endif; ?>
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
</a></li>

View File

@@ -50,22 +50,25 @@
<?php
$_dVal = (float)$data["duration_value"];
$_dUnit = $data["duration_unit"];
$_unitLabels = [
'pages' => 'pages',
'minutes' => 'minutes',
'sec' => 'secondes',
'heures' => 'heures',
'mo' => 'Mo',
];
$_label = $_unitLabels[$_dUnit] ?? $_dUnit;
// if float, show 0.1 or .0 as needed
$_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal;
$_label = match($_dUnit) {
'pages' => 'pages',
'mo' => 'Mo',
'durée' => '',
default => $_dUnit,
};
if ($_dUnit === 'durée') {
$_hours = (int)floor($_dVal);
$_mins = (int)round(($_dVal - $_hours) * 60);
$_display = ($_mins > 0) ? "{$_hours}h{$_mins}" : "{$_hours}h";
} else {
$_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal;
}
?>
<p class="tfe-meta-item">
<span class="tfe-meta-label">Durée :</span>
<?= $_display ?> <?= htmlspecialchars($_label) ?>
<?= $_display ?><?= $_label ? ' ' . htmlspecialchars($_label) : '' ?>
</p>
<?php unset($_unitLabels, $_dVal, $_dUnit, $_label, $_display); endif; ?>
<?php unset($_dVal, $_dUnit, $_label, $_display, $_hours, $_mins); endif; ?>
<?php if (!empty($data["languages"])): ?>
<p class="tfe-meta-item">