Ajuste l'onglet Pages : bouton d'ajout fixe en haut, retire les flèches de réordonnancement, style d'onglet simplifié

- "+ Ajouter une page" passe avant la liste et reste toujours visible
  (flex-shrink:0), seule la liste défile désormais.
- Retire les boutons ↑/↓ par rangée : le glisser-déposer suffit pour
  réordonner, ce qui laisse plus de place à l'affichage du nom de la
  page. forgeDocMovePage devient mort (plus aucun appelant) et est
  supprimé.
- Style de l'onglet actif simplifié : seul le border-bottom change de
  couleur, le texte reste neutre (plus de changement de couleur du
  libellé).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-23 09:33:45 +02:00
co-authored by Claude Sonnet 5
parent b8e7a4c672
commit 763d26c1f1
4 changed files with 31 additions and 52 deletions
+1 -1
View File
@@ -152,7 +152,7 @@ SonarQube : voir section 2, sous-section "SonarQube" — CI restaurée (non-bloq
| `static/game/js/play/offline/filter-repeater-rows.js:24` (`forgeDecodeClauses`) | `javascript:S2486` | **Corrigé, même patron que ci-dessus** : `_filtres_json` est un attribut rendu par le serveur, jamais tapé à la main — un JSON invalide y trahit presque toujours un bug côté éditeur/serveur. `console.warn('_filtres_json invalide, filtre ignoré', e)` ajouté, comportement inchangé (repli sur l'ancien format à 2 filtres fixes ou aucun filtre). Couvert par un nouveau test (`filter-repeater-rows.test.js`, `forgeDecodeClauses — _filtres_json invalide`). | Lot 4 "modernisation JS", 18/09/2026 | | `static/game/js/play/offline/filter-repeater-rows.js:24` (`forgeDecodeClauses`) | `javascript:S2486` | **Corrigé, même patron que ci-dessus** : `_filtres_json` est un attribut rendu par le serveur, jamais tapé à la main — un JSON invalide y trahit presque toujours un bug côté éditeur/serveur. `console.warn('_filtres_json invalide, filtre ignoré', e)` ajouté, comportement inchangé (repli sur l'ancien format à 2 filtres fixes ou aucun filtre). Couvert par un nouveau test (`filter-repeater-rows.test.js`, `forgeDecodeClauses — _filtres_json invalide`). | Lot 4 "modernisation JS", 18/09/2026 |
| `static/game/js/play/offline/filter-repeater-rows.js:59`, `:70` (`forgeResolveVariablePath`, JSON.parse + navigation `.champ`/`[index]`) | `javascript:S2486` | **Documenté, pas corrigé — nature différente du cas ci-dessus** : ici `rawValue` est la VALEUR ACTUELLE d'une variable de jeu (modifiable librement par n'importe quelle action "Modifier une variable"), pas une config interne à l'éditeur — un chemin qui ne correspond pas à sa forme actuelle est un cas normal et attendu (ex. variable encore à sa valeur par défaut non-JSON), déjà explicitement documenté par le commentaire de la fonction ("Ne lève jamais... même convention que côté serveur"). Un `console.warn` ici bruiterait la console à chaque usage légitime. | Lot 4 "modernisation JS", 18/09/2026 | | `static/game/js/play/offline/filter-repeater-rows.js:59`, `:70` (`forgeResolveVariablePath`, JSON.parse + navigation `.champ`/`[index]`) | `javascript:S2486` | **Documenté, pas corrigé — nature différente du cas ci-dessus** : ici `rawValue` est la VALEUR ACTUELLE d'une variable de jeu (modifiable librement par n'importe quelle action "Modifier une variable"), pas une config interne à l'éditeur — un chemin qui ne correspond pas à sa forme actuelle est un cas normal et attendu (ex. variable encore à sa valeur par défaut non-JSON), déjà explicitement documenté par le commentaire de la fonction ("Ne lève jamais... même convention que côté serveur"). Un `console.warn` ici bruiterait la console à chaque usage légitime. | Lot 4 "modernisation JS", 18/09/2026 |
| `static/document/js/document-editor.js` (`FORGE_DOC_STYLE_PRESETS`, `FORGE_DOC_SHAPE_KINDS`, `FORGE_DOC_SNAP_SIZE` — 3 sites) | `eslint:no-var`, `eslint:vars-on-top` | Constantes de premier niveau déclarées en `var` plutôt que `const` : un `<script src>` de page est rejoué TEL QUEL par `pjax.js` (`runScriptsIn`) à chaque navigation interne — une redéclaration `let`/`const` au premier niveau lèverait `SyntaxError: already declared` à la 2e exécution (voir l'en-tête de `static/pjax.js`, et le commentaire d'en-tête de ce fichier). `var` est le seul mot-clé sûr à ce niveau ; tout le reste du fichier (état mutable, y compris à l'intérieur des fonctions) est bien en `let`/`const`, porté par `window.forgeDocState` plutôt que par des variables de module (même convention que `static/game/js/scenes/scene-editor.js` et les autres scripts de page existants, qui n'ont eux aucune constante de ce genre à déclarer). | Session du 20/09/2026 | | `static/document/js/document-editor.js` (`FORGE_DOC_STYLE_PRESETS`, `FORGE_DOC_SHAPE_KINDS`, `FORGE_DOC_SNAP_SIZE` — 3 sites) | `eslint:no-var`, `eslint:vars-on-top` | Constantes de premier niveau déclarées en `var` plutôt que `const` : un `<script src>` de page est rejoué TEL QUEL par `pjax.js` (`runScriptsIn`) à chaque navigation interne — une redéclaration `let`/`const` au premier niveau lèverait `SyntaxError: already declared` à la 2e exécution (voir l'en-tête de `static/pjax.js`, et le commentaire d'en-tête de ce fichier). `var` est le seul mot-clé sûr à ce niveau ; tout le reste du fichier (état mutable, y compris à l'intérieur des fonctions) est bien en `let`/`const`, porté par `window.forgeDocState` plutôt que par des variables de module (même convention que `static/game/js/scenes/scene-editor.js` et les autres scripts de page existants, qui n'ont eux aucune constante de ce genre à déclarer). | Session du 20/09/2026 |
| `templates/document/document_edit.html` (`.docPageRow`, rangées de l'onglet "Pages" du panneau gauche) + `static/document/js/document-editor.js` (`forgeDocRenderPageManagerList`) | `Web:S6819`, `Web:MouseEventWithoutKeyboardEquivalentCheck` (Sonar) | `div role="button" tabindex="0"` volontaire : chaque rangée contient de vrais `<button>` d'action (monter/descendre/renommer/supprimer, voir `.docPageRowActions`), qu'un `<button>` englobant ne pourrait pas contenir validement (imbrication de `<button>` invalide, le parseur HTML referme le bouton englobant trop tôt — même défaut structurel déjà rencontré et corrigé de la même façon ailleurs dans ce fichier). L'équivalent clavier (Entrée/Espace déclenche `forgeDocSwitchPage`, même effet que le clic) est posé côté JS (`row.addEventListener('keydown', ...)`), donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne voit pas les `addEventListener` attachés dynamiquement. Vérifié par un test jsdom dédié (rôle `button`, équivalent clavier fonctionnel). | Session du 21/09/2026 ; renommé (panneau à onglets) le 23/09/2026 | | `templates/document/document_edit.html` (`.docPageRow`, rangées de l'onglet "Pages" du panneau gauche) + `static/document/js/document-editor.js` (`forgeDocRenderPageManagerList`) | `Web:S6819`, `Web:MouseEventWithoutKeyboardEquivalentCheck` (Sonar) | `div role="button" tabindex="0"` volontaire : chaque rangée contient de vrais `<button>` d'action (renommer/supprimer, voir `.docPageRowActions` — le réordonnancement, ancien 3ᵉ/4ᵉ bouton monter/descendre, est passé au glisser-déposer seul le 23/09/2026, retour utilisateur : "je pouvais changer l'ordre des pages en glisser déposer donc les flèches [...] sont inutiles"), qu'un `<button>` englobant ne pourrait pas contenir validement (imbrication de `<button>` invalide, le parseur HTML referme le bouton englobant trop tôt — même défaut structurel déjà rencontré et corrigé de la même façon ailleurs dans ce fichier). L'équivalent clavier (Entrée/Espace déclenche `forgeDocSwitchPage`, même effet que le clic) est posé côté JS (`row.addEventListener('keydown', ...)`), donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne voit pas les `addEventListener` attachés dynamiquement. Vérifié par un test jsdom dédié (rôle `button`, équivalent clavier fonctionnel). | Session du 21/09/2026 ; renommé (panneau à onglets) le 23/09/2026 ; boutons monter/descendre retirés le 23/09/2026 |
### Détail — `javascript:S8786` (ReDoS), lot 1 "modernisation JS" ### Détail — `javascript:S8786` (ReDoS), lot 1 "modernisation JS"
+13 -1
View File
@@ -1192,7 +1192,9 @@ img.docImage {
} }
.docSidebarTab.is-active { .docSidebarTab.is-active {
color: var(--doc-accent); /* Le plus simple des styles d'onglet Bulma (retour utilisateur du
23/09/2026) : SEUL le border-bottom change de couleur, le texte
reste neutre (pas de changement de couleur sur le libellé). */
border-bottom-color: var(--doc-accent); border-bottom-color: var(--doc-accent);
} }
@@ -1218,10 +1220,20 @@ img.docImage {
gap: 8px; gap: 8px;
} }
/* Bouton d'ajout EN HAUT, toujours visible (retour utilisateur du
23/09/2026 : "le bouton pour ajouter une page dois être en haut
toujours visible") — flex-shrink:0 pour ne jamais être compressé par
.docPageManagerList, seule cette dernière défile. */
.docPageManagerAdd {
flex-shrink: 0;
}
.docPageManagerList { .docPageManagerList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
flex: 1;
min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
+7 -30
View File
@@ -512,23 +512,6 @@ function forgeDocDeletePage(pageId) {
}); });
} }
function forgeDocMovePage(pageId, direction) {
const ordered = forgeDocOrderedPages();
const index = ordered.findIndex((p) => p.id === pageId);
const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= ordered.length) return;
forgeDocApiPageMove(pageId, newIndex).then(() => {
const moved = window.forgeDocState.pages.find((p) => p.id === pageId);
const rest = window.forgeDocState.pages
.filter((p) => p.id !== pageId)
.sort((a, b) => a.order_index - b.order_index);
rest.splice(newIndex, 0, moved);
rest.forEach((p, i) => { p.order_index = i; });
window.forgeDocState.pages = rest;
forgeDocRenderPageManagerList();
});
}
function forgeDocSelectAllText(el) { function forgeDocSelectAllText(el) {
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(el); range.selectNodeContents(el);
@@ -595,11 +578,13 @@ function forgeDocRenderPageManagerList() {
if (!container) return; if (!container) return;
const pages = forgeDocOrderedPages(); const pages = forgeDocOrderedPages();
// div + role="button" ici, jamais <button> : la rangée contient de // div + role="button" ici, jamais <button> : la rangée contient de
// vrais <button> d'action (monter/descendre/renommer/supprimer), qu'un // vrais <button> d'action (renommer/supprimer), qu'un <button>
// <button> englobant ne pourrait pas contenir validement. L'équivalent // englobant ne pourrait pas contenir validement. L'équivalent clavier
// clavier (Entrée/Espace) est posé plus bas. Réordonnancement : glisser // (Entrée/Espace) est posé plus bas. Réordonnancement UNIQUEMENT par
// (souris, voir forgeDocBindPageManagerDrag) ET boutons ↑/↓ (clavier), // glisser (voir forgeDocBindPageManagerDrag) — pas de boutons monter/
// les deux appellent le même forgeDocMovePage. // descendre (retour utilisateur du 23/09/2026 : "je pouvais changer
// l'ordre des pages en glisser déposer donc les flèches [...] sont
// inutiles [...] donner plus de place à l'affichage du nom").
container.innerHTML = pages.map((p, index) => ` container.innerHTML = pages.map((p, index) => `
<div class="docPageRow ${p.id === window.forgeDocState.activePageId ? 'is-active' : ''}" <div class="docPageRow ${p.id === window.forgeDocState.activePageId ? 'is-active' : ''}"
role="button" role="button"
@@ -610,8 +595,6 @@ function forgeDocRenderPageManagerList() {
<span class="docPageRowNumber">${index + 1}</span> <span class="docPageRowNumber">${index + 1}</span>
<span class="docPageRowTitle"><span class="docPageRowTitleText">${forgeDocEscapeHtml(p.title)}</span></span> <span class="docPageRowTitle"><span class="docPageRowTitleText">${forgeDocEscapeHtml(p.title)}</span></span>
<span class="docPageRowActions"> <span class="docPageRowActions">
<button type="button" class="docPageRowMove" data-dir="up" data-page-id="${p.id}" aria-label="Monter" title="Monter"${index === 0 ? ' disabled' : ''}>↑</button>
<button type="button" class="docPageRowMove" data-dir="down" data-page-id="${p.id}" aria-label="Descendre" title="Descendre"${index === pages.length - 1 ? ' disabled' : ''}>↓</button>
<button type="button" class="docPageRowRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button> <button type="button" class="docPageRowRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button>
<button type="button" class="docPageRowDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button> <button type="button" class="docPageRowDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button>
</span> </span>
@@ -629,12 +612,6 @@ function forgeDocRenderPageManagerList() {
forgeDocSwitchPage(Number(row.dataset.pageId)); forgeDocSwitchPage(Number(row.dataset.pageId));
}); });
}); });
container.querySelectorAll('.docPageRowMove').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
forgeDocMovePage(Number(btn.dataset.pageId), btn.dataset.dir);
});
});
container.querySelectorAll('.docPageRowRename').forEach((btn) => { container.querySelectorAll('.docPageRowRename').forEach((btn) => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
+10 -20
View File
@@ -123,6 +123,11 @@
role="tabpanel" role="tabpanel"
aria-labelledby="docTabPages" aria-labelledby="docTabPages"
hidden> hidden>
{# Bouton d'ajout EN HAUT, toujours visible (retour utilisateur du
23/09/2026 : "le bouton pour ajouter une page dois être en haut
toujours visible" — avec beaucoup de pages, il disparaissait en
bas de la liste défilante). #}
<button type="button" class="docBtnSecondary docPageManagerAdd" id="docPageManagerAdd">+ Ajouter une page</button>
<div class="docPageManagerList" id="docPageManagerList"> <div class="docPageManagerList" id="docPageManagerList">
{% for page in pages %} {% for page in pages %}
<div class="docPageRow {{ 'is-active' if page.id == active_page.id else '' }}" <div class="docPageRow {{ 'is-active' if page.id == active_page.id else '' }}"
@@ -131,29 +136,15 @@
draggable="true" draggable="true"
data-page-id="{{ page.id }}"> data-page-id="{{ 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 (réordonner/renommer/ volontaire : la rangée contient de vrais <button> d'action (renommer/supprimer),
supprimer), qu'un <button> englobant ne pourrait pas contenir validement. qu'un <button> englobant ne pourrait pas contenir validement. L'équivalent clavier
L'équivalent clavier (Entrée/Espace) est posé côté JS (forgeDocRenderPageManagerList), (Entrée/Espace) est posé côté JS (forgeDocRenderPageManagerList), donc le finding
donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne clavier de Sonar est un faux positif : l'analyseur statique ne voit pas les
voit pas les addEventListener dynamiques. #} addEventListener dynamiques. #}
<span class="docPageRowHandle" aria-hidden="true">⠿</span> <span class="docPageRowHandle" aria-hidden="true">⠿</span>
<span class="docPageRowNumber">{{ loop.index }}</span> <span class="docPageRowNumber">{{ loop.index }}</span>
<span class="docPageRowTitle"><span class="docPageRowTitleText">{{ page.title }}</span></span> <span class="docPageRowTitle"><span class="docPageRowTitleText">{{ page.title }}</span></span>
<span class="docPageRowActions"> <span class="docPageRowActions">
<button type="button"
class="docPageRowMove"
data-dir="up"
data-page-id="{{ page.id }}"
aria-label="Monter"
title="Monter"
{{ 'disabled' if loop.first else '' }}>↑</button>
<button type="button"
class="docPageRowMove"
data-dir="down"
data-page-id="{{ page.id }}"
aria-label="Descendre"
title="Descendre"
{{ 'disabled' if loop.last else '' }}>↓</button>
<button type="button" <button type="button"
class="docPageRowRename" class="docPageRowRename"
data-page-id="{{ page.id }}" data-page-id="{{ page.id }}"
@@ -169,7 +160,6 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<button type="button" class="docBtnSecondary docPageManagerAdd" id="docPageManagerAdd">+ Ajouter une page</button>
</div> </div>
<div class="docSidebarTabPanel" id="docTabPanelLayout" role="tabpanel" aria-labelledby="docTabLayout"> <div class="docSidebarTabPanel" id="docTabPanelLayout" role="tabpanel" aria-labelledby="docTabLayout">
{% for category_key, category in element_library.items() %} {% for category_key, category in element_library.items() %}