admin: semantic HTML pass — checkbox fieldset, landmarks, dl/dt, autocomplete, inline styles

checkbox-list.php partial:
- Replace outer <div>/<label> with <div>/<span class="admin-row-label"> + inner
  <fieldset class="admin-checkbox-group"><legend class="sr-only"> to satisfy
  WCAG 1.3.1 (group label for multi-checkbox rows without duplicating visible text)
- Replace <div class="admin-checkbox-list"> with <ul>; each checkbox wrapped in <li>

admin.css:
- Drop .admin-checkbox-list; add .admin-body fieldset.admin-checkbox-group rules
  (border/padding reset so it doesn't inherit jury-fieldset box styling)
- Extend form-row label rule to span.admin-row-label
- .admin-inline-form + .admin-inline-form { margin-top:.35rem } replaces inline style
- .admin-input--inline / .admin-select--inline get width:160px (was inline style)
- .admin-tags-count + table th sizing via :has() replaces th inline styles

login.php: wrap content in <main id="main-content"> (missing landmark)

account.php:
- <div class="admin-account-status"> → <dl>; __label <span> → <dt>
- <div class="admin-danger-zone__description"> → <p>

index.php: <div class="admin-maintenance-bar"> → <aside role="status" aria-label="Statut du site">

add.php / edit.php: autocomplete="name" on author field, autocomplete="email" on
contact field (WCAG 1.3.5 / input purpose)

tags.php: all inline style= attributes removed (width, text-align, margin-top,
display:inline); all moved to CSS classes
This commit is contained in:
Pontoporeia
2026-04-02 21:06:20 +02:00
parent fde05da493
commit ff8e33727d
11 changed files with 111 additions and 56 deletions

View File

@@ -23,15 +23,17 @@ if (empty($_SESSION['csrf_token'])) {
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<!-- Status info -->
<div class="admin-account-status">
<dl class="admin-account-status">
<div class="admin-account-status__row">
<span class="admin-account-status__label">Authentification PHP</span>
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
<dt class="admin-account-status__label">Authentification PHP</dt>
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
</div>
<div class="admin-account-status__row">
<span class="admin-account-status__label">Fichier de configuration</span>
<code class="admin-account-status__code">config/admin_credentials.php</code>
<?php $badgeType = 'ok'; $badgeValue = file_exists($credentialsFile); $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
<dt class="admin-account-status__label">Fichier de configuration</dt>
<dd>
<code class="admin-account-status__code">config/admin_credentials.php</code>
<?php $badgeType = 'ok'; $badgeValue = file_exists($credentialsFile); $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
</dd>
</div>
<?php if (!$hasPassword): ?>
<p class="admin-account-status__note">
@@ -39,7 +41,7 @@ if (empty($_SESSION['csrf_token'])) {
<code>config/admin_credentials.php</code> avec un hash bcrypt.
</p>
<?php endif; ?>
</div>
</dl>
<!-- Password change form -->
<h2 class="admin-section-title"><?= $hasPassword ? 'Changer le mot de passe' : 'Définir le mot de passe' ?></h2>
@@ -86,13 +88,13 @@ if (empty($_SESSION['csrf_token'])) {
<!-- Danger zone: remove password -->
<h2 class="admin-section-title admin-section-title--danger">Zone de danger</h2>
<div class="admin-danger-zone">
<div class="admin-danger-zone__description">
<p class="admin-danger-zone__description">
<strong>Supprimer la configuration du mot de passe PHP</strong><br>
<small>
Supprime <code>config/admin_credentials.php</code>. L'accès admin
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
</small>
</div>
</p>
<form method="post" action="/admin/actions/account.php"
onsubmit="return confirm('Supprimer le fichier de mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">

View File

@@ -52,8 +52,8 @@ function wasSelected($key, $value) {
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>

View File

@@ -70,8 +70,8 @@ try {
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact :'; $value = ''; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php $name = 'mail'; $label = 'Contact :'; $value = ''; $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
<?php
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;

View File

@@ -71,7 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
<!-- Maintenance mode toggle -->
<?php $maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag'); ?>
<div class="admin-maintenance-bar <?= $maintenanceOn ? 'admin-maintenance-bar--active' : '' ?>">
<aside role="status" class="admin-maintenance-bar <?= $maintenanceOn ? 'admin-maintenance-bar--active' : '' ?>" aria-label="Statut du site">
<?php if ($maintenanceOn): ?>
<span>⚠ Mode maintenance <strong>activé</strong> — le site public est inaccessible.</span>
<form method="post" action="actions/maintenance.php" style="display:inline;">
@@ -90,7 +90,7 @@ document.addEventListener('DOMContentLoaded', () => {
</button>
</form>
<?php endif; ?>
</div>
</aside>
<!-- Stats (always reflects full DB, independent of active filters) -->
<dl class="admin-stats">

View File

@@ -26,6 +26,7 @@ $pageTitle = 'Connexion';
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<div class="admin-login-wrap">
<div class="admin-login-box">
<h2>Administration</h2>
@@ -43,5 +44,6 @@ $pageTitle = 'Connexion';
</form>
</div>
</div>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -31,16 +31,16 @@ try {
<table>
<thead>
<tr>
<th scope="col" style="width:40%;">Nom</th>
<th scope="col" style="width:12%;text-align:center;">TFE associés</th>
<th scope="col" style="width:48%;">Actions</th>
<th scope="col">Nom</th>
<th scope="col">TFE associés</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($tags as $tag): ?>
<tr>
<td><?= htmlspecialchars($tag['name']) ?></td>
<td style="text-align:center;"><?= (int)$tag['thesis_count'] ?></td>
<td class="admin-tags-count"><?= (int)$tag['thesis_count'] ?></td>
<td>
<!-- Rename -->
<form method="post" action="actions/tag.php" class="admin-inline-form">
@@ -48,16 +48,16 @@ try {
<input type="hidden" name="action" value="rename">
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">
<input class="admin-input--inline" type="text" name="new_name"
value="<?= htmlspecialchars($tag['name']) ?>" required style="width:160px;">
value="<?= htmlspecialchars($tag['name']) ?>" required>
<button type="submit" class="admin-btn admin-btn--sm">Renommer</button>
</form>
<!-- Merge into another tag -->
<form method="post" action="actions/tag.php" class="admin-inline-form" style="margin-top:.35rem;">
<form method="post" action="actions/tag.php" class="admin-inline-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="merge">
<input type="hidden" name="source_id" value="<?= (int)$tag['id'] ?>">
<select name="target_id" class="admin-select--inline" style="width:160px;" required>
<select name="target_id" class="admin-select--inline" required>
<option value="">— Fusionner dans… —</option>
<?php foreach ($tags as $other): ?>
<?php if ($other['id'] !== $tag['id']): ?>
@@ -72,7 +72,7 @@ try {
</form>
<!-- Delete -->
<form method="post" action="actions/tag.php" class="admin-inline-form" style="margin-top:.35rem;display:inline;">
<form method="post" action="actions/tag.php" class="admin-inline-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">

View File

@@ -54,7 +54,8 @@
border-bottom: 1px solid var(--border-primary);
}
.admin-form > div:not(.admin-submit-wrap) > label {
.admin-form > div:not(.admin-submit-wrap) > label,
.admin-form > div:not(.admin-submit-wrap) > span.admin-row-label {
font-size: 0.92rem;
padding-top: 0.5rem;
font-weight: 400;
@@ -140,12 +141,28 @@
display: block;
}
/* Checkboxes */
.admin-checkbox-list {
/* Checkbox group fieldset (languages, formats)
Wraps the <ul> of checkboxes; the visible label is a sibling <span>
in the grid row. The <legend> repeats the label text for AT only (sr-only).
.admin-body scope ensures this overrides the generic .admin-body fieldset rule. */
.admin-body fieldset.admin-checkbox-group {
border: none;
padding: 0;
margin: 0;
background: transparent;
}
.admin-body fieldset.admin-checkbox-group > ul {
list-style: none;
margin: 0;
padding-top: 0.3rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
padding-top: 0.3rem;
}
.admin-body fieldset.admin-checkbox-group > ul > li {
display: contents; /* let the inner label handle layout */
}
.admin-checkbox-label {
@@ -754,13 +771,26 @@
.admin-input--inline,
.admin-inline-form input[type="text"] {
font-size: 0.82rem;
width: 160px;
}
.admin-select--inline,
.admin-inline-form select {
font-size: 0.82rem;
width: 160px;
}
/* Stack secondary forms (merge, delete) below the rename form */
.admin-inline-form + .admin-inline-form {
margin-top: 0.35rem;
}
/* Tags table column sizing */
.admin-body table:has(.admin-tags-count) th:nth-child(1) { width: 40%; }
.admin-body table:has(.admin-tags-count) th:nth-child(2) { width: 12%; }
.admin-body table:has(.admin-tags-count) th:nth-child(3) { width: 48%; }
.admin-tags-count { text-align: center; }
/* ── Banner preview ─────────────────────────────────────────────────────── */
.admin-banner-preview img {
max-width: 320px;