Bande de pages : un seul carré visible (la page active), les flèches changent de page
Build and deploy / test-python (push) Successful in 9m16s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / lint-python (push) Successful in 4m11s
Build and deploy / lint-js (push) Failing after 1m23s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 4m14s

Remplace le bandeau scrollable multi-cartes par un vrai carrousel
une-page-à-la-fois : un seul carré affiché (la page active), les
grandes flèches ‹/› aux extrémités changent directement la page
affichée (au lieu de juste faire défiler la vue). Les petites flèches
←/→ au-dessus du carré restent pour réordonner la page active parmi
ses sœurs, sans changer l'affichage — distinctes des flèches de
navigation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-22 15:08:09 +02:00
co-authored by Claude Sonnet 5
parent a2b95bd547
commit e33b11458c
3 changed files with 117 additions and 91 deletions
+23 -19
View File
@@ -1165,22 +1165,20 @@ img.docImage {
/* ---- Section "Pages" du panneau GAUCHE (retour utilisateur du /* ---- Section "Pages" du panneau GAUCHE (retour utilisateur du
21/09/2026 : "une section qui s'ajoute dans le panneau de gauche pour 21/09/2026 : "une section qui s'ajoute dans le panneau de gauche pour
ajouter une page et naviguer entre elles", à la place de l'ex- ajouter une page et naviguer entre elles", à la place de l'ex-
catégorie "Mise en page"/formes libres, supprimée) — un bandeau catégorie "Mise en page"/formes libres, supprimée) — un CARROUSEL
HORIZONTAL défilant à hauteur FIXE (retour utilisateur du 21/09/2026 : une-page-à-la-fois (retour utilisateur du 22/09/2026 : "un seul carré
"ce système n'est pas bien il faut un système de slide horizontale" — page visible, les flèches ne servent pas qu'à naviguer mais aussi à
une liste verticale grandissait sans limite avec beaucoup de pages), changer de page") : un seul carré = la page ACTIVE, hauteur fixe quel
toujours visible en tête du panneau. Cartes texte (pas de vignette que soit le nombre de pages. Carte texte (pas de vignette visuelle). ---- */
visuelle). ---- */
.docPageSection { .docPageSection {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
/* Bandeau : flèches précédent/suivant AUX EXTRÉMITÉS (retour utilisateur /* ‹/› CHANGENT la page affichée (voir forgeDocBindPageStripNav) — à ne
du 21/09/2026 : "affiche les flèches ... en plus", donc en supplément pas confondre avec les petites flèches ←/→ DANS la carte, qui elles
des ←/→ par carte, qui eux réordonnent) — font défiler la VUE de la réordonnent la page active parmi ses sœurs sans changer l'affichage. */
bande, jamais la page active. */
.docPageStrip { .docPageStrip {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1190,7 +1188,7 @@ img.docImage {
.docPageStripNav { .docPageStripNav {
flex-shrink: 0; flex-shrink: 0;
width: 22px; width: 22px;
height: 72px; height: 84px;
border: 1px solid var(--doc-border); border: 1px solid var(--doc-border);
border-radius: 6px; border-radius: 6px;
background: var(--doc-card); background: var(--doc-card);
@@ -1202,19 +1200,21 @@ img.docImage {
justify-content: center; justify-content: center;
} }
.docPageStripNav:hover { .docPageStripNav:hover:not(:disabled) {
border-color: var(--doc-accent); border-color: var(--doc-accent);
color: var(--doc-text); color: var(--doc-text);
} }
.docPageStripNav:disabled {
opacity: 0.3;
cursor: default;
}
.docPageList { .docPageList {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: row; justify-content: center;
gap: 6px;
overflow: auto hidden;
padding-bottom: 4px;
} }
.docPageListItem { .docPageListItem {
@@ -1222,9 +1222,8 @@ img.docImage {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
flex-shrink: 0; width: 88px;
width: 72px; padding: 6px;
padding: 4px;
border: 1px solid var(--doc-border); border: 1px solid var(--doc-border);
border-radius: 8px; border-radius: 8px;
background: var(--doc-card); background: var(--doc-card);
@@ -1336,6 +1335,11 @@ img.docImage {
color: var(--doc-text); color: var(--doc-text);
} }
.docPageListCounter {
font-size: 10px;
color: var(--doc-muted);
}
/* ---- RIGHT SIDEBAR : Propriétés ---- */ /* ---- RIGHT SIDEBAR : Propriétés ---- */
.docField { .docField {
display: flex; display: flex;
+40 -22
View File
@@ -592,27 +592,32 @@ function forgeDocRenderPageList() {
const container = document.getElementById('docPageList'); const container = document.getElementById('docPageList');
if (!container) return; if (!container) return;
const pages = forgeDocOrderedPages(); const pages = forgeDocOrderedPages();
// div + role="button" ici, jamais <button> : la rangée contient de const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
// vrais <button> d'action (monter/descendre/renommer/supprimer), qu'un const p = pages[index];
// <button> englobant ne pourrait pas contenir validement. L'équivalent // Un seul carré visible : la page ACTIVE (retour utilisateur du
// clavier (Entrée/Espace) est posé plus bas. // 22/09/2026 : "un seul carré page visible") — les grandes flèches
container.innerHTML = pages.map((p, index) => ` // ‹/› de .docPageStrip CHANGENT la page affichée (voir
<div class="docPageListItem ${p.id === window.forgeDocState.activePageId ? 'is-active' : ''}" // forgeDocBindPageStripNav), distinctes des petites flèches ←/→
role="button" // ci-dessous qui réordonnent la page active parmi ses sœurs sans
tabindex="0" // changer l'affichage. div + role="button" ici, jamais <button> : la
data-page-id="${p.id}"> // carte contient de vrais <button> d'action, qu'un <button> englobant
// ne pourrait pas contenir validement. L'équivalent clavier
// (Entrée/Espace) est posé plus bas.
container.innerHTML = `
<div class="docPageListItem is-active" role="button" tabindex="0" data-page-id="${p.id}">
<div class="docPageListActions"> <div class="docPageListActions">
<button type="button" class="docPageListMove" data-dir="up" data-page-id="${p.id}" aria-label="Déplacer vers la gauche" title="Déplacer vers la gauche"${index === 0 ? ' disabled' : ''}>←</button> <button type="button" class="docPageListMove" data-dir="up" data-page-id="${p.id}" aria-label="Réordonner vers la gauche" title="Réordonner vers la gauche"${index === 0 ? ' disabled' : ''}>←</button>
<button type="button" class="docPageListMove" data-dir="down" data-page-id="${p.id}" aria-label="Déplacer vers la droite" title="Déplacer vers la droite"${index === pages.length - 1 ? ' disabled' : ''}>→</button> <button type="button" class="docPageListMove" data-dir="down" data-page-id="${p.id}" aria-label="Réordonner vers la droite" title="Réordonner vers la droite"${index === pages.length - 1 ? ' disabled' : ''}>→</button>
<button type="button" class="docPageListRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button> <button type="button" class="docPageListRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button>
<button type="button" class="docPageListDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button> <button type="button" class="docPageListDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button>
</div> </div>
<div class="docPageListSquare">${index + 1}</div> <div class="docPageListSquare">${index + 1}</div>
<span class="docPageListTitle"><span class="docPageListTitleText">${forgeDocEscapeHtml(p.title)}</span></span> <span class="docPageListTitle"><span class="docPageListTitleText">${forgeDocEscapeHtml(p.title)}</span></span>
<span class="docPageListCounter">${index + 1} / ${pages.length}</span>
</div> </div>
`).join(''); `;
container.querySelectorAll('.docPageListItem[data-page-id]').forEach((item) => { const item = container.querySelector('.docPageListItem[data-page-id]');
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
if (e.target.closest('.docPageListActions')) return; if (e.target.closest('.docPageListActions')) return;
forgeDocSwitchPage(Number(item.dataset.pageId)); forgeDocSwitchPage(Number(item.dataset.pageId));
@@ -622,7 +627,6 @@ function forgeDocRenderPageList() {
e.preventDefault(); e.preventDefault();
forgeDocSwitchPage(Number(item.dataset.pageId)); forgeDocSwitchPage(Number(item.dataset.pageId));
}); });
});
container.querySelectorAll('.docPageListMove').forEach((btn) => { container.querySelectorAll('.docPageListMove').forEach((btn) => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -642,21 +646,35 @@ function forgeDocRenderPageList() {
}); });
}); });
container.querySelectorAll('.docPageListTitle').forEach(forgeDocApplyTitleMarquee); container.querySelectorAll('.docPageListTitle').forEach(forgeDocApplyTitleMarquee);
const prevBtn = document.getElementById('docPageStripPrev');
const nextBtn = document.getElementById('docPageStripNext');
if (prevBtn) prevBtn.disabled = index <= 0;
if (nextBtn) nextBtn.disabled = index >= pages.length - 1;
const addBtn = document.getElementById('docPageListAdd'); const addBtn = document.getElementById('docPageListAdd');
if (addBtn) addBtn.onclick = () => forgeDocAddPage(); if (addBtn) addBtn.onclick = () => forgeDocAddPage();
} }
function forgeDocBindPageStripNav() { function forgeDocBindPageStripNav() {
const container = document.getElementById('docPageList');
const prevBtn = document.getElementById('docPageStripPrev'); const prevBtn = document.getElementById('docPageStripPrev');
const nextBtn = document.getElementById('docPageStripNext'); const nextBtn = document.getElementById('docPageStripNext');
if (!container || !prevBtn || !nextBtn) return; if (!prevBtn || !nextBtn) return;
// Fait défiler la VUE de la bande (pas la page active — retour // Les flèches ‹/› CHANGENT la page affichée (retour utilisateur du
// utilisateur du 21/09/2026 : "affiche les flèches ... en plus", donc // 22/09/2026 : "les flèches ne servent pas qu'à naviguer mais aussi à
// une navigation visuelle distincte des ←/→ par carte qui, eux, // changer de page") — lecture dynamique de l'ordre courant à chaque
// réordonnent). Défilement d'environ une carte + son espacement. // clic (même principe que forgeDocBindPageNavPill), jamais un index
prevBtn.addEventListener('click', () => container.scrollBy({ left: -110, behavior: 'smooth' })); // figé au moment du binding.
nextBtn.addEventListener('click', () => container.scrollBy({ left: 110, behavior: 'smooth' })); prevBtn.addEventListener('click', () => {
const pages = forgeDocOrderedPages();
const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
if (index > 0) forgeDocSwitchPage(pages[index - 1].id);
});
nextBtn.addEventListener('click', () => {
const pages = forgeDocOrderedPages();
const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
if (index < pages.length - 1) forgeDocSwitchPage(pages[index + 1].id);
});
} }
function forgeDocBindPageNavPill() { function forgeDocBindPageNavPill() {
+30 -26
View File
@@ -101,54 +101,58 @@
<button type="button" <button type="button"
class="docPageStripNav" class="docPageStripNav"
id="docPageStripPrev" id="docPageStripPrev"
aria-label="Défiler vers la gauche" aria-label="Page précédente"
title="Défiler vers la gauche">‹</button> title="Page précédente">‹</button>
<div class="docPageList" id="docPageList"> <div class="docPageList" id="docPageList">
{% for page in pages %} {# active_page est toujours pages[0] au chargement initial (voir
<div class="docPageListItem {{ 'is-active' if page.id == active_page.id else '' }}" routes/document/document_edit.py) — index 0 fixe, jamais recalculé
role="button" ici ; un changement de page ensuite est entièrement géré côté
tabindex="0" client par forgeDocRenderPageList, qui recalcule le VRAI index. #}
data-page-id="{{ page.id }}"> {% set active_index = 0 %}
<div class="docPageListItem is-active" role="button" tabindex="0" data-page-id="{{ active_page.id }}">
{# //NOSONAR Web:S6819,Web:MouseEventWithoutKeyboardEquivalentCheck - div+role=button {# //NOSONAR Web:S6819,Web:MouseEventWithoutKeyboardEquivalentCheck - div+role=button
volontaire : la rangée contient de vrais <button> d'action (monter/descendre/ volontaire : la rangée contient de vrais <button> d'action (réordonner/renommer/
renommer/supprimer), qu'un <button> englobant ne pourrait pas contenir supprimer), qu'un <button> englobant ne pourrait pas contenir validement.
validement. L'équivalent clavier (Entrée/Espace) est posé côté JS L'équivalent clavier (Entrée/Espace) est posé côté JS (forgeDocRenderPageList),
(forgeDocRenderPageList), donc le finding clavier de Sonar est un faux positif donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne
: l'analyseur statique ne voit pas les addEventListener dynamiques. #} voit pas les addEventListener dynamiques. #}
<div class="docPageListActions"> <div class="docPageListActions">
<button type="button" <button type="button"
class="docPageListMove" class="docPageListMove"
data-dir="up" data-dir="up"
data-page-id="{{ page.id }}" data-page-id="{{ active_page.id }}"
aria-label="Déplacer vers la gauche" aria-label="Réordonner vers la gauche"
title="Déplacer vers la gauche">←</button> title="Réordonner vers la gauche"
{{ 'disabled' if active_index == 0 else '' }}>←</button>
<button type="button" <button type="button"
class="docPageListMove" class="docPageListMove"
data-dir="down" data-dir="down"
data-page-id="{{ page.id }}" data-page-id="{{ active_page.id }}"
aria-label="Déplacer vers la droite" aria-label="Réordonner vers la droite"
title="Déplacer vers la droite">→</button> title="Réordonner vers la droite"
{{ 'disabled' if active_index == pages|length - 1 else '' }}>→</button>
<button type="button" <button type="button"
class="docPageListRename" class="docPageListRename"
data-page-id="{{ page.id }}" data-page-id="{{ active_page.id }}"
aria-label="Renommer cette page" aria-label="Renommer cette page"
title="Renommer cette page">✎</button> title="Renommer cette page">✎</button>
<button type="button" <button type="button"
class="docPageListDelete" class="docPageListDelete"
data-page-id="{{ page.id }}" data-page-id="{{ active_page.id }}"
aria-label="Supprimer cette page" aria-label="Supprimer cette page"
title="Supprimer cette page">✕</button> title="Supprimer cette page"
{{ 'disabled' if pages|length <= 1 else '' }}>✕</button>
</div> </div>
<div class="docPageListSquare">{{ loop.index }}</div> <div class="docPageListSquare">{{ active_index + 1 }}</div>
<span class="docPageListTitle"><span class="docPageListTitleText">{{ page.title }}</span></span> <span class="docPageListTitle"><span class="docPageListTitleText">{{ active_page.title }}</span></span>
<span class="docPageListCounter">{{ active_index + 1 }} / {{ pages|length }}</span>
</div> </div>
{% endfor %}
</div> </div>
<button type="button" <button type="button"
class="docPageStripNav" class="docPageStripNav"
id="docPageStripNext" id="docPageStripNext"
aria-label="Défiler vers la droite" aria-label="Page suivante"
title="Défiler vers la droite">›</button> title="Page suivante">›</button>
</div> </div>
<button type="button" class="docBtnSecondary docPageListAdd" id="docPageListAdd">+ Ajouter une page</button> <button type="button" class="docBtnSecondary docPageListAdd" id="docPageListAdd">+ Ajouter une page</button>
</div> </div>