mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
Semantic HTML: home page card grid — <ul>/<li>/<figure>/<nav> refactor
Replace presentational divs in index.php and main.css with elements that carry correct semantic meaning, fixing multiple WCAG 2.1 AA issues: index.php: - <div class="cards-container"> → <ul class="cards-container"> (list of navigable items) - <a class="card-link"><div class="card">…</div></a> → <li class="card"><a> (block link is the <a>, <li> is the container; removes the redundant .card div wrapper) - <div class="card__media"> → <figure class="card__media"> when wrapping an <img>; gradient placeholder stays as <div> (presentational, aria-hidden) - Improved alt text: "Couverture — [title] par [authors]" instead of bare title - Removed <div class="card__info"> wrapper; caption is now a bare <p class="card__caption"> directly inside the <a> - <div class="filter-info"> → <p class="filter-info" role="status"> (live-region semantics; announces filter state to screen readers) - ✕ symbol in clear-filter link wrapped in <span aria-hidden="true"> - Gradient placeholder div gets aria-hidden="true" (decorative; caption below carries text) - Empty-state <p style="…"> → <li class="cards-empty"> (removes inline style) - <div class="pagination-wrap"> → <nav class="pagination-wrap" aria-label="Pagination"> with <ul>/<li> children; page-info <span> → <li aria-current="page"> main.css: - .cards-container: add list-style:none; margin:0; padding:0 (reset <ul> defaults) - Remove .card-link rule; replace with .card > a (block flex link, no separate class) - .card__media: add margin:0 to reset <figure> default margin - Remove .card__info rules; rename .authors to .card__caption with same styles - Add .cards-empty rule (removes last inline style from index.php) - .pagination-wrap: restructured for <nav>/<ul>; inner <ul> carries the flex layout - prefers-reduced-motion: add .card__media--gradient guard WCAG criteria addressed: 1.1.1 (alt text), 1.3.1 (info & relationships via semantic list/figure), 2.4.1 (filter-info now live region), role="status" on filter banner.
This commit is contained in:
16
TODO.md
16
TODO.md
@@ -498,7 +498,7 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis
|
|||||||
`.tfe-back-link`, `.tfe-restricted` in `tfe.css`
|
`.tfe-back-link`, `.tfe-restricted` in `tfe.css`
|
||||||
- `admin/edit.php`: multiple `style=` on `.admin-form-row` and banner preview → modifier
|
- `admin/edit.php`: multiple `style=` on `.admin-form-row` and banner preview → modifier
|
||||||
classes in `admin.css`
|
classes in `admin.css`
|
||||||
- `index.php` line 146: `style="padding:2rem;color:#666;"` → `.cards-empty` in `main.css`
|
- [x] `index.php` line 146: `style="padding:2rem;color:#666;"` → `.cards-empty` in `main.css`
|
||||||
|
|
||||||
- [x] **`.site-nav__right` is a duplicate of `.site-nav__link`** - removed `.site-nav__right` block
|
- [x] **`.site-nav__right` is a duplicate of `.site-nav__link`** - removed `.site-nav__right` block
|
||||||
from `common.css`; updated `nav.php` to use `.site-nav__link` on the À Propos link.
|
from `common.css`; updated `nav.php` to use `.site-nav__link` on the À Propos link.
|
||||||
@@ -580,27 +580,27 @@ The design does **not** need to change - only the vocabulary of the markup.
|
|||||||
|
|
||||||
### II - `public/index.php`
|
### II - `public/index.php`
|
||||||
|
|
||||||
- [ ] **`<div class="filter-info">`** is a status/notice banner. Use `<p role="status">` or
|
- [x] **`<div class="filter-info">`** is a status/notice banner. Use `<p role="status">` or
|
||||||
`<output>` - both carry live-region semantics for screen readers without extra ARIA.
|
`<output>` - both carry live-region semantics for screen readers without extra ARIA.
|
||||||
|
|
||||||
- [ ] **`<div class="cards-container">`** is a list of navigable items. Replace with `<ul>` -
|
- [x] **`<div class="cards-container">`** is a list of navigable items. Replace with `<ul>` -
|
||||||
removing the wrapper div and making each card an `<li>`. `.cards-container` → target `main > ul`
|
removing the wrapper div and making each card an `<li>`. `.cards-container` → target `main > ul`
|
||||||
or a single class on `<ul>`.
|
or a single class on `<ul>`.
|
||||||
|
|
||||||
- [ ] **`<a class="card-link"><div class="card">...</div></a>`** - the outer `<a>` wrapping a `<div>`
|
- [x] **`<a class="card-link"><div class="card">...</div></a>`** - the outer `<a>` wrapping a `<div>`
|
||||||
makes the div redundant. The `<a>` is already a block element (set `display:block`). The
|
makes the div redundant. The `<a>` is already a block element (set `display:block`). The
|
||||||
`.card` div can be removed; CSS targets `ul li a` directly. The `<li>` inside the `<ul>`
|
`.card` div can be removed; CSS targets `ul li a` directly. The `<li>` inside the `<ul>`
|
||||||
becomes the card container.
|
becomes the card container.
|
||||||
|
|
||||||
- [ ] **`<div class="card__media">`** - this is the image/media wrapper inside each card.
|
- [x] **`<div class="card__media">`** - this is the image/media wrapper inside each card.
|
||||||
When it contains an `<img>`, use `<figure>` (a self-contained media unit). When it shows
|
When it contains an `<img>`, use `<figure>` (a self-contained media unit). When it shows
|
||||||
the gradient placeholder (no real image), a plain `<div>` is fine since it's presentational.
|
the gradient placeholder (no real image), a plain `<div>` is fine since it's presentational.
|
||||||
|
|
||||||
- [ ] **`<div class="card__info"><p class="authors">...</p></div>`** - the `.card__info` wrapper
|
- [x] **`<div class="card__info"><p class="authors">...</p></div>`** - the `.card__info` wrapper
|
||||||
exists only to add padding. Move the padding to the `<p>` or `<li>` directly; remove the
|
exists only to add padding. Move the padding to the `<p>` or `<li>` directly; remove the
|
||||||
div. The `<p>` stays. `.authors` class → either keep it or target `li > p`.
|
div. The `<p>` stays. `.authors` class → either keep it or target `li > p`.
|
||||||
|
|
||||||
- [ ] **`<div class="pagination-wrap">`** with `<a class="pagination-btn">` and
|
- [x] **`<div class="pagination-wrap">`** with `<a class="pagination-btn">` and
|
||||||
`<span class="pagination-info">` - replace with `<nav aria-label="Pagination"><ul>...</ul></nav>`.
|
`<span class="pagination-info">` - replace with `<nav aria-label="Pagination"><ul>...</ul></nav>`.
|
||||||
Each button becomes an `<li>`. The disabled state uses `aria-disabled="true"` +
|
Each button becomes an `<li>`. The disabled state uses `aria-disabled="true"` +
|
||||||
`tabindex="-1"` instead of a `.disabled` class alone (which has no keyboard semantics).
|
`tabindex="-1"` instead of a `.disabled` class alone (which has no keyboard semantics).
|
||||||
@@ -927,7 +927,7 @@ Current state: **zero ARIA attributes, zero skip links, zero focus-visible style
|
|||||||
|
|
||||||
#### 1.1.1 Non-text content (alt text)
|
#### 1.1.1 Non-text content (alt text)
|
||||||
|
|
||||||
- [ ] **Home card images use the thesis title as `alt`** - `alt="<?= $item['title'] ?>"` is a
|
- [x] **Home card images use the thesis title as `alt`** - `alt="<?= $item['title'] ?>"` is a
|
||||||
reasonable fallback, but the title alone provides no context about what the image depicts.
|
reasonable fallback, but the title alone provides no context about what the image depicts.
|
||||||
Prefer `"Couverture - [titre] par [auteurs]"` for cover images, or `""` (empty) for purely
|
Prefer `"Couverture - [titre] par [auteurs]"` for cover images, or `""` (empty) for purely
|
||||||
decorative banners where the caption below already carries all the text information.
|
decorative banners where the caption below already carries all the text information.
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1400px) {
|
@media (min-width: 1400px) {
|
||||||
@@ -36,13 +39,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Each card = media thumbnail + text below */
|
/* Each card = list item containing a block <a> link */
|
||||||
.card-link {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -52,12 +49,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card > a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* card__media is a <figure> (with image) or <div> (gradient placeholder) */
|
||||||
.card__media {
|
.card__media {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 4/3;
|
aspect-ratio: 4/3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #e8e8e8;
|
background: #e8e8e8;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0; /* reset <figure> default margin */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__media img,
|
.card__media img,
|
||||||
@@ -120,26 +127,22 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__info {
|
/* Card caption — <p> directly inside the <a>, below the media */
|
||||||
|
.card__caption {
|
||||||
padding: 0.55rem 0.5rem 0.65rem;
|
padding: 0.55rem 0.5rem 0.65rem;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
}
|
|
||||||
|
|
||||||
.card__info .authors {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__info .title {
|
/* Empty-state message shown when no cards exist */
|
||||||
margin: 0;
|
.cards-empty {
|
||||||
font-weight: 400;
|
padding: 2rem;
|
||||||
color: var(--text-muted);
|
color: #6b6b6b;
|
||||||
display: -webkit-box;
|
font-size: 0.9rem;
|
||||||
-webkit-line-clamp: 2;
|
list-style: none;
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter info */
|
/* Filter info */
|
||||||
@@ -169,14 +172,19 @@
|
|||||||
|
|
||||||
/* Pagination */
|
/* Pagination */
|
||||||
.pagination-wrap {
|
.pagination-wrap {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--white);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-top: 1px solid var(--border-color);
|
list-style: none;
|
||||||
background: var(--white);
|
margin: 0;
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn {
|
.pagination-btn {
|
||||||
@@ -226,4 +234,7 @@
|
|||||||
.card:hover .card__media video {
|
.card:hover .card__media video {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
.card__media--gradient {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
public/index.php
137
public/index.php
@@ -69,89 +69,98 @@ $extraCss = ['assets/main.css'];
|
|||||||
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
|
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
|
||||||
|
|
||||||
<?php if ($year): ?>
|
<?php if ($year): ?>
|
||||||
<div class="filter-info">
|
<p class="filter-info" role="status">
|
||||||
Année : <?= htmlspecialchars($year) ?>
|
Année : <?= htmlspecialchars($year) ?>
|
||||||
<a href="index.php" class="clear-filter">✕ Réinitialiser</a>
|
<a href="index.php" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
|
||||||
</div>
|
</p>
|
||||||
<?php elseif ($isDefaultView && !empty($latestYear)): ?>
|
<?php elseif ($isDefaultView && !empty($latestYear)): ?>
|
||||||
<div class="filter-info home-latest-label">
|
<p class="filter-info home-latest-label" role="status">
|
||||||
Découvrez les TFE de <?= (int)$latestYear ?> — sélection aléatoire
|
Découvrez les TFE de <?= (int)$latestYear ?> — sélection aléatoire
|
||||||
</div>
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<main class="home-main" id="main-content">
|
<main class="home-main" id="main-content">
|
||||||
<div class="cards-container">
|
<ul class="cards-container">
|
||||||
<?php foreach ($itemsToLoad as $item): ?>
|
<?php foreach ($itemsToLoad as $item): ?>
|
||||||
<a href="tfe.php?id=<?= (int)$item["id"] ?>" class="card-link">
|
<li class="card">
|
||||||
<div class="card">
|
<a href="tfe.php?id=<?= (int)$item["id"] ?>">
|
||||||
<div class="card__media">
|
<?php
|
||||||
<?php
|
// Resolve thumbnail: banner_path → cover file → gradient placeholder
|
||||||
// Resolve thumbnail: banner_path → cover file → gradient placeholder
|
$thumb = null;
|
||||||
$thumb = null;
|
|
||||||
|
|
||||||
// 1. Banner path (dedicated home thumbnail)
|
// 1. Banner path (dedicated home thumbnail)
|
||||||
if (!empty($item['banner_path'])) {
|
if (!empty($item['banner_path'])) {
|
||||||
$thumb = $item['banner_path'];
|
$thumb = $item['banner_path'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cover image from thesis_files (batch-loaded above)
|
// 2. Cover image from thesis_files (batch-loaded above)
|
||||||
if (!$thumb && isset($coverMap[$item['id']])) {
|
if (!$thumb && isset($coverMap[$item['id']])) {
|
||||||
$thumb = $coverMap[$item['id']];
|
$thumb = $coverMap[$item['id']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fall through to gradient
|
// 3. Fall through to gradient
|
||||||
?>
|
?>
|
||||||
<?php if ($thumb): ?>
|
<?php if ($thumb): ?>
|
||||||
|
<figure class="card__media">
|
||||||
<img src="/media.php?path=<?= urlencode($thumb) ?>"
|
<img src="/media.php?path=<?= urlencode($thumb) ?>"
|
||||||
alt="<?= htmlspecialchars($item['title']) ?>"
|
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
<?php else: ?>
|
</figure>
|
||||||
<?php
|
<?php else: ?>
|
||||||
$hue = ($item['id'] * 47 + 160) % 360;
|
<?php
|
||||||
$hue2 = ($hue + 40) % 360;
|
$hue = ($item['id'] * 47 + 160) % 360;
|
||||||
?>
|
$hue2 = ($hue + 40) % 360;
|
||||||
<div class="card__media--gradient"
|
?>
|
||||||
style="background:linear-gradient(135deg,hsl(<?= $hue ?>,55%,40%),hsl(<?= $hue2 ?>,50%,28%));">
|
<div class="card__media card__media--gradient"
|
||||||
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
style="background:linear-gradient(135deg,hsl(<?= $hue ?>,55%,40%),hsl(<?= $hue2 ?>,50%,28%));"
|
||||||
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
|
aria-hidden="true">
|
||||||
</div>
|
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
||||||
<?php endif; ?>
|
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__info">
|
<?php endif; ?>
|
||||||
<p class="authors"><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> – <?php endif; ?><?= htmlspecialchars($item["title"]) ?></p>
|
<p class="card__caption"><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> – <?php endif; ?><?= htmlspecialchars($item["title"]) ?></p>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php if (empty($itemsToLoad)): ?>
|
<?php if (empty($itemsToLoad)): ?>
|
||||||
<p style="padding:2rem;color:#666;">Aucun mémoire trouvé.</p>
|
<li class="cards-empty">Aucun mémoire trouvé.</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
<?php if ($totalPages > 1): ?>
|
<?php if ($totalPages > 1): ?>
|
||||||
<div class="pagination-wrap">
|
<nav class="pagination-wrap" aria-label="Pagination">
|
||||||
<?php $yearParam = $year ? '&year=' . (int)$year : ''; ?>
|
<?php $yearParam = $year ? '&year=' . (int)$year : ''; ?>
|
||||||
<a href="?page=1<?= $yearParam ?>"
|
<ul>
|
||||||
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
|
<li>
|
||||||
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<a href="?page=1<?= $yearParam ?>"
|
||||||
aria-label="Première page">«</a>
|
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
|
||||||
<a href="?page=<?= max(1, $page - 1) . $yearParam ?>"
|
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||||||
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
|
aria-label="Première page">«</a>
|
||||||
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
</li>
|
||||||
aria-label="Page précédente">‹</a>
|
<li>
|
||||||
<span class="pagination-info">
|
<a href="?page=<?= max(1, $page - 1) . $yearParam ?>"
|
||||||
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
|
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
|
||||||
</span>
|
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||||||
<a href="?page=<?= min($totalPages, $page + 1) . $yearParam ?>"
|
aria-label="Page précédente">‹</a>
|
||||||
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
|
</li>
|
||||||
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<li class="pagination-info" aria-current="page">
|
||||||
aria-label="Page suivante">›</a>
|
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
|
||||||
<a href="?page=<?= $totalPages . $yearParam ?>"
|
</li>
|
||||||
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
|
<li>
|
||||||
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<a href="?page=<?= min($totalPages, $page + 1) . $yearParam ?>"
|
||||||
aria-label="Dernière page">»</a>
|
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
|
||||||
</div>
|
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||||||
|
aria-label="Page suivante">›</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="?page=<?= $totalPages . $yearParam ?>"
|
||||||
|
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
|
||||||
|
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
||||||
|
aria-label="Dernière page">»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user