fix(mobile): bouton mode lecture visible uniquement fichier ouvert (BUG-016)
CI / lint (push) Successful in 1m10s
CI / security (push) Successful in 46s
CI / test (push) Successful in 2m23s
CI / build (push) Successful in 41s
CI / e2e (push) Successful in 10m28s

This commit is contained in:
2026-09-12 22:44:15 -04:00
parent cb07b92543
commit d47fcbf2be
7 changed files with 95 additions and 4 deletions
+8
View File
@@ -251,6 +251,14 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Corrigé
- **BUG-016 — Mobile : le bouton « mode lecture » recouvre le bouton d'envoi de l'assistant** —
le bouton flottant `📖` (`#me-reading-btn`, `z-index: 890`) passait au-dessus du panneau
assistant plein écran sur mobile (`z-index: 100`) et masquait le bouton d'envoi `✈️`. Il
n'apparaît désormais que lorsqu'un **fichier est ouvert** (`state.currentPath`) **et** que le
panneau assistant est fermé ; sa visibilité est resynchronisée à chaque mutation de
`#content-area` et sur les événements `bookslm:opened`/`bookslm:closed`. Tests JSDOM
(`tests/frontend/mobile-editor.test.mjs` +2) et E2E mis à jour.
- **BUG-015 — Assistant IA : sélection des menus `/` et `@` invisible au clavier** — l'élément
actif utilisait `background: var(--surface2)`, or `--surface2` est **identique** à
`--bg-primary` (fond du menu) dans le thème sombre par défaut : la ligne sélectionnée par les
+2
View File
@@ -125,6 +125,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-013* | Assistant IA : les liens fichiers/répertoires des réponses affichent « Aucun vault actif pour ouvrir ce lien » | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | En mode Général, cliquer un lien de fichier/dossier dans une réponse | `_openFileLink()` et `_revealPath()` utilisent `_activeVault()` (vault de contexte → vault sélectionné → premier vault) au lieu de `_resolveVault()` seul | `_resolveVault()` est nul sans document ouvert ; les liens échouaient en mode Général. Tests : `tests/frontend/ai.test.mjs` (+1) |
| *BUG-014* | Assistant IA : les flèches ↑/↓ ne naviguent pas dans les menus `/` et `@` | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Ouvrir `/`, puis ↑/↓ (y compris après un clic hors zone de texte) | Navigation gérée au niveau du **panneau en phase de capture** (`panel.addEventListener('keydown', ..., true)`), donc indépendante du focus | Le `keydown` n'était écouté que sur la zone de texte ; dès que le focus changeait, les flèches étaient ignorées. Tests : `tests/frontend/ai.test.mjs` (+1) |
| *BUG-015* | Assistant IA : la ligne sélectionnée des menus `/` et `@` est invisible au clavier | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/style.css` | Ouvrir `/` ou `@`, naviguer avec ↑/↓ | État `.active` en `--bg-hover` + barre d'accent à gauche (`inset 3px 0 0 var(--accent)`) au lieu de `--surface2` | `--surface2` est identique à `--bg-primary` (fond du menu) en thème sombre : la sélection ne se voyait pas. Idem pour la liste de modèles |
| *BUG-016* | Mobile : le bouton « mode lecture » flottant recouvre le bouton d'envoi de l'assistant IA | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/mobile-editor.js`, `frontend/style.css` | Mobile, sans fichier ouvert : ouvrir l'assistant puis constater que `📖` masque `✈️` | `updateReadingButtonVisibility()` n'affiche `#me-reading-btn` que si `state.currentPath` est défini **et** que le panneau assistant est fermé ; resynchronisé à chaque mutation de `#content-area` et sur `bookslm:opened`/`bookslm:closed` ; règle `.me-reading-btn[hidden]{display:none}` | Le bouton flottant (z-index 890) passait au-dessus du panneau plein écran mobile (z-index 100). Tests : `tests/frontend/mobile-editor.test.mjs` (+2) |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -156,6 +157,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-12 | BUG-015, #82 | Correction + UX | `frontend/js/bookslm.js`, `frontend/style.css`, `tests/frontend/ai.test.mjs` | BUG-015 : sélection des menus `/` et `@` rendue visible (`--bg-hover` + barre d'accent à gauche) car `--surface2` = `--bg-primary`. #82 : retrait de l'indice clavier sous la zone de saisie. Vérifié Playwright (contraste `#1f2430` vs `#0f1117` + barre accent) et tests frontend 47/47. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-12 | #82 | Amélioration perf + UX | `backend/services/search.py`, `backend/main.py`, `frontend/js/bookslm.js`, `frontend/js/ui.js`, `frontend/style.css`, `tests/test_api_main.py`, `tests/frontend/ai.test.mjs` | #82 : endpoint `GET /api/vault/{vault}/paths` + préchargement et filtrage client du menu `@` (instantané) ; sélecteurs Fournisseur/Modèle agrandis (0,8 rem / 34 px) ; « 🧠 BooksLM » ajouté au menu contextuel de la racine des vaults. Vérifié : pytest 151 (ciblés) + ruff/mypy, tests frontend 49/49. | ✅ livré (en attente vérif utilisateur) |
| 2026-09-12 | #82 | UX | `frontend/js/ai.js`, `frontend/style.css`, `tests/frontend/ai.test.mjs` | Sélecteurs Fournisseur/Modèle alignés à droite dans la barre de l'assistant et réordonnés : capacité du modèle (ⓘ) → fournisseur → modèle. Bulle de capacités ouverte vers la droite (`left: 0`). Test JSDOM de l'ordre. Vérifié : tests frontend 49/49. | ✅ livré (en attente vérif utilisateur) |
| 2026-09-12 | BUG-016 | Correction | `frontend/js/mobile-editor.js`, `frontend/style.css`, `tests/frontend/mobile-editor.test.mjs`, `tests/e2e/mobile-editor.spec.js` | BUG-016 : le bouton « mode lecture » n'apparaît que si un fichier est ouvert et que l'assistant est fermé, sinon il recouvrait le bouton d'envoi sur mobile. Vérifié : tests frontend 24/24 (mobile) + suite JSDOM verte. | 🟢 corrigé (en attente vérif utilisateur) |
---
+11 -4
View File
@@ -1,6 +1,6 @@
# #69 - Éditeur mobile natif — Interface tactile optimisée
> **Statut :** ✅ Terminé — 100 % implémenté + 22 tests frontend (2026-09-11)
> **Statut :** ✅ Terminé — 100 % implémenté + 24 tests frontend (2026-09-12)
> **Effort :** 2-3 jours (réalisé) | **Impact :** 🟢
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog](../../CHANGELOG.md)
@@ -10,7 +10,7 @@
- `frontend/index.html` — section « Édition mobile » du guide intégré + lien de navigation
- `frontend/locales/{fr,en}.json` — clés `mobile_editor.*` et `help.mobile_editor_*`
- `frontend/js/app.js` — appel `initMobileEditor()` au démarrage
- `tests/frontend/mobile-editor.test.mjs` — 22 tests JSDOM (helpers + câblage DOM)
- `tests/frontend/mobile-editor.test.mjs` — 24 tests JSDOM (helpers + câblage DOM)
- `.gitea/workflows/ci.yml` — test ajouté au job `lint`
- **Description :** sur téléphone et tablette, l'édition et la lecture s'adaptent au tactile :
@@ -100,17 +100,24 @@ Le bouton flottant `📖` (`#me-reading-btn`) bascule `body.reading-mode` :
borné avec message si extrémité atteinte) ;
- l'état est mémorisé (`obsigate-reading-mode`) et restauré au rechargement sur mobile.
**Visibilité conditionnelle (BUG-016).** Le bouton `📖` n'est affiché que si un **fichier est
ouvert** (`state.currentPath`) **et** que le panneau assistant est fermé : sinon il passait
au-dessus du panneau mobile plein écran et masquait le bouton d'envoi. `updateReadingButtonVisibility()`
est appelé à l'init, à chaque mutation de `#content-area` (MutationObserver) et sur les événements
`bookslm:opened` / `bookslm:closed`.
---
## Tests
`tests/frontend/mobile-editor.test.mjs` (JSDOM, 22 tests) couvre :
`tests/frontend/mobile-editor.test.mjs` (JSDOM, 24 tests) couvre :
- `FORMAT_ACTIONS`, `clampZoom`, `detectSwipe` (seuil, dominance verticale, gauche/droite) ;
- `formatChange` pour bold/italic/code/liste/lien, avec et sans sélection, toggle et ligne unique ;
- `insertText`, `applyFormatToTextarea`, `applyEditorFontSize` (clamp + persistance) ;
- le câblage DOM : construction de `#me-toolbar`, idempotence, contrôles du mode lecture,
bascule `toggleReadingMode`/`isReadingMode`.
bascule `toggleReadingMode`/`isReadingMode`, et visibilité conditionnelle de `#me-reading-btn`
(fichier ouvert + assistant fermé).
Les chemins réseau (backlinks, fichiers adjacents) et les gestes réels restent à valider en E2E
navigateur / test manuel mobile.
+34
View File
@@ -603,6 +603,27 @@ function buildReadingControls() {
document.body.appendChild(exit);
}
/** True when the BooksLM assistant panel is expanded on screen. */
function isAssistantOpen() {
const panel = document.querySelector('.bookslm-panel');
return !!(panel && panel.classList.contains('open') && !panel.classList.contains('hidden'));
}
/**
* Show the floating reading-mode button only while a file is open and the
* assistant panel is closed. Without this guard the button floats above the
* full-screen mobile assistant and covers its send button.
*
* Called on init, whenever the content area changes and when the assistant
* opens/closes.
*/
export function updateReadingButtonVisibility() {
const btn = document.getElementById('me-reading-btn');
if (!btn) return;
const fileOpen = !!state.currentPath;
btn.hidden = !fileOpen || isAssistantOpen();
}
// ── Swipe gestures ─────────────────────────────────────────────────────────
function initSwipeGestures() {
@@ -659,6 +680,19 @@ export function initMobileEditor() {
buildReadingControls();
initSwipeGestures();
// Keep the reading button in sync with the current view: visible only when
// a file is open and the assistant is closed.
updateReadingButtonVisibility();
document.addEventListener('bookslm:opened', updateReadingButtonVisibility);
document.addEventListener('bookslm:closed', updateReadingButtonVisibility);
const readingArea = document.getElementById('content-area');
if (readingArea && typeof MutationObserver !== 'undefined') {
new MutationObserver(updateReadingButtonVisibility).observe(readingArea, {
childList: true,
subtree: true,
});
}
const bodyEl = document.getElementById('editor-body');
if (bodyEl) {
initPinchZoom(bodyEl);
+4
View File
@@ -9577,6 +9577,10 @@ body.bookslm-resizing { cursor: ew-resize; user-select: none; }
.me-reading-exit {
display: none;
}
/* Author `display: flex` would otherwise win over the `[hidden]` attribute. */
.me-reading-btn[hidden] {
display: none;
}
@media (max-width: 768px) {
.me-reading-btn {
+5
View File
@@ -30,6 +30,11 @@ test.describe('Mobile editor (#69)', () => {
await boot(page);
const enter = page.locator('#me-reading-btn');
// The floating button only shows once a file is open (and not on the
// dashboard), so it cannot cover the assistant's send button.
await expect(enter).toBeHidden();
await page.evaluate(() => window.TabManager.open('TestVault', 'note1.md'));
await expect(enter).toBeVisible();
await enter.click();
+31
View File
@@ -14,6 +14,7 @@ import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import { fileURLToPath, pathToFileURL } from "node:url";
import path from "node:path";
import { state } from "../../frontend/js/state.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -62,6 +63,7 @@ const {
initMobileEditor,
toggleReadingMode,
isReadingMode,
updateReadingButtonVisibility,
} = mod;
let testCount = 0;
@@ -223,5 +225,34 @@ test("reading mode exposes enter and exit controls", () => {
assert.ok(w.document.getElementById("me-reading-exit"), "exit button exists");
});
test("reading button only shows while a file is open", () => {
const btn = w.document.getElementById("me-reading-btn");
state.currentPath = null;
updateReadingButtonVisibility();
assert.ok(btn.hidden, "hidden without an open file");
state.currentPath = "Notes/note.md";
updateReadingButtonVisibility();
assert.ok(!btn.hidden, "shown when a file is open");
state.currentPath = null;
updateReadingButtonVisibility();
assert.ok(btn.hidden, "hidden again once the file is closed");
});
test("reading button is hidden while the assistant panel is open", () => {
const btn = w.document.getElementById("me-reading-btn");
const panel = w.document.createElement("div");
panel.className = "bookslm-panel open";
w.document.body.appendChild(panel);
state.currentPath = "Notes/note.md";
updateReadingButtonVisibility();
assert.ok(btn.hidden, "hidden while the assistant is expanded");
panel.classList.add("hidden");
updateReadingButtonVisibility();
assert.ok(!btn.hidden, "shown again once the assistant is collapsed");
panel.remove();
state.currentPath = null;
updateReadingButtonVisibility();
});
console.log(`\n${passCount}/${testCount} tests passed\n`);
process.exit(passCount === testCount ? 0 : 1);