feat: refonte mobile de la section Configurations #114
This commit is contained in:
+31
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||
> [Unreleased](#unreleased). La dernière version livrée est **2.22.1**.
|
||||
> [Unreleased](#unreleased). La dernière version livrée est **2.23.0**.
|
||||
|
||||
---
|
||||
|
||||
@@ -14,6 +14,36 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [2.23.0] — 2026-09-24
|
||||
|
||||
### Ajouté
|
||||
|
||||
- **#114 — Configuration, refonte mobile-responsive.** La page « Configurations »
|
||||
est désormais pensée pour le tactile (≤ 768 px) : modale **plein écran**
|
||||
(`100dvh`, safe-area), sommaire en **drawer coulissant** gauche
|
||||
(`position: fixed`, largeur `min(320px, 88vw)`) avec fond assombri
|
||||
(`#config-modal.config-toc-open::before`) et bouton de fermeture
|
||||
`#config-toc-close` (tap sur le backdrop ou Échap ferme le drawer d'abord,
|
||||
puis la modale) ; formulaires sur **une colonne** ; tous les boutons
|
||||
(save/secondary/danger/add/sm/hamburger/fermer) et liens du sommaire en cibles
|
||||
**≥ 44 px** ; champs et selects en **16 px + min-height 44 px** (anti-zoom
|
||||
iOS) ; rangée « Sauvegarder / Réindexer / Réinitialiser » **sticky** en bas de
|
||||
sa section avec safe-area ; MFA (codes de secours en 1 colonne, champ de code
|
||||
full-width et wrap des rangées d'action) ; débordements webhook/token/share
|
||||
corrigés. Dettes HTML/i18n de l'audit incluses : `.config-actions-row` replacée
|
||||
dans `#cfg-backend-settings` (+ `</section>` orphelin supprimé), id dupliqué
|
||||
`cfg-partages-publics` retiré du `<h2>`, conteneur mort
|
||||
`#plugins-settings-container` supprimé, libellé `#mt-explorer` i18n
|
||||
(`settings.explorer`), doublons `.config-btn-add` et règle morte
|
||||
`.mfa-recovery-input` purgés ; i18n : clés mortes `settings.backend`,
|
||||
`settings.backend_hint`, `settings.restart_badge`, `settings.save`,
|
||||
`settings.plugins` supprimées, `config.toc_close` ajoutée, `settings.tabs` FR
|
||||
corrigé (« Onglets »). Tests : `config-mobile.test.mjs` étendu (27, au CI) +
|
||||
E2E `config-mobile.spec.js` (5, projet `chromium-mobile`). Fiche :
|
||||
[docs/features/settings-mobile-114.md](./docs/features/settings-mobile-114.md).
|
||||
|
||||
---
|
||||
|
||||
## [2.22.1] — 2026-09-23
|
||||
|
||||
### Corrigé
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](https://www.python.org/)
|
||||
@@ -975,8 +975,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.22.1).
|
||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.23.0).
|
||||
|
||||
---
|
||||
|
||||
*Projet : ObsiGate | Version : 2.22.1 | Dernière mise à jour : Septembre 2026*
|
||||
*Projet : ObsiGate | Version : 2.23.0 | Dernière mise à jour : Septembre 2026*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](https://www.python.org/)
|
||||
@@ -1150,8 +1150,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.22.1).
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.23.0).
|
||||
|
||||
---
|
||||
|
||||
*Project: ObsiGate | Version: 2.22.1 | Last updated: September 2026*
|
||||
*Project: ObsiGate | Version: 2.23.0 | Last updated: September 2026*
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.22.1"
|
||||
version = "2.23.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.22.1"
|
||||
version = "2.23.0"
|
||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||
authors = ["Bruno Charest"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||
"productName": "ObsiGate",
|
||||
"version": "2.22.1",
|
||||
"version": "2.23.0",
|
||||
"identifier": "com.obsigate.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../frontend",
|
||||
|
||||
@@ -262,6 +262,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
| 2026-09-23 | BUG-071 (complément E2E) | Test | `tests/e2e/config-mobile.spec.js` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-071 (complément E2E)** : spec Playwright mobile (convention `mobile-editor.spec.js` : `test.skip` hors viewport ≤768px, donc inactive sur le projet `chromium-desktop` du CI). Vérifié en local sur l'instance de test (port 2029, auth désactivée) : hamburger → sommaire, sélection → scroll + actif + repli, 0 débordement horizontal à 393px (3/3 `chromium-mobile`, 3 ignorés en desktop) ; suite `mobile-editor.spec.js` intacte (3/3). | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-23 | BUG-072 | Correction | `frontend/js/viewer.js`, `frontend/style.css`, `tests/frontend/image-viewer.test.mjs`, `tests/e2e/image-viewer.spec.js`, `scripts/run-e2e-local.ps1` (nouveau), `package.json`, `AGENTS.md`, `README.md`, `README.fr.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-072** : dans la visionneuse d'images (#108-D), le plein écran (lightbox) et le panneau « Métadonnées » étaient perdus dès qu'on changeait d'image avec ←/→ (ou la pellicule), car `openFile` → `renderFile` recrée entièrement `renderImageViewer`. (1) **Persistance** : état module `_imageViewerState { lightbox, meta }` restauré à chaque rendu ; un drapeau `_imageViewerNavPending` posé par `go()` et le clic de vignette indique à `renderFile` que le rendu suivant est une navigation image→image (pas de réinitialisation) — toute autre ouverture repart à zéro. (2) **Panneau latéral** : `.image-meta-panel` déplacé dans un nouveau `.image-viewer-body` en flex row, à droite de `.image-stage` (`border-left`, `width:280px; max-width:40%`, défilement vertical) au lieu d'une bande sous la pellicule ; la règle lightbox ne masque plus que la pellicule. Boutons stables `image-btn-lightbox`/`image-btn-metadata` + `aria-pressed`, `Escape` resynchronise l'état. Tests statiques `image-viewer.test.mjs` (+2) et E2E Playwright (+1). **Diagnostic E2E** : `npm run test:e2e` bloquait car `bash` résout vers WSL (HS, Ubuntu `Stopped`, `HCS_E_CONNECTION_TIMEOUT`) et git-bash est bloqué par App Control → lanceur PowerShell ajouté. Vérifié : `image-viewer.spec.js` 4/4, **suite `chromium-desktop` complète 103 passed / 6 skipped (10,3 min)** via `scripts/run-e2e-local.ps1`, `image-viewer.test.mjs` 12/12, unit 10/10, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-23 | BUG-073 | Correction | `frontend/style.css`, `tests/frontend/mobile-toolbar.test.mjs` (nouveau), `tests/e2e/mobile-toolbar.spec.js` (nouveau), `.gitea/workflows/ci.yml`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-073** : en mobile (≤768px), la barre fixe `#mobile-toolbar` (64px + safe-area) recouvrait le bas de tous les documents/pages — fin de contenu inaccessible. (1) **Cause** : la règle de clearance du bloc `@media (max-width: 768px)` ciblait `.main-layout`, classe absente de `index.html` (vrai conteneur : `.main-body`) → sélecteur mort, zéro dégagement ; règle sœur morte `.editor-modal.active ~ .main-layout { padding-bottom: 0 }` supprimée (overlay plein écran / nécessaire en édition inline). (2) **Correctif** : `.main-body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0)) }`, reset `body.reading-mode .main-body { padding-bottom: 0 }` (barre masquée en mode lecture), `body.np-active .content-area` ramené de `calc(64px+safe+76px)` à `76px` (dégagement dock seul — géométrie totale identique, pas de double comptage avec `.main-body`). Tests : `mobile-toolbar.test.mjs` (7 statiques, ajouté au CI), E2E `mobile-toolbar.spec.js` (géométrie + scroll fin de `ANALYSE_REVIEW.md`, skip hors viewport ≤768). Vérifié : `mobile-toolbar` 7/7, JSDOM 14 suites 0 échec, **E2E `chromium-mobile` BUG-073 2/2 + régressions mobile-editor/config-mobile 6/6**, **suite `chromium-desktop` complète 106 passed / 9 skipped (11,4 min)**, pytest 1302 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-24 | #114 | Feature | `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs`, `tests/e2e/config-mobile.spec.js`, `docs/features/settings-mobile-114.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **#114 — Configuration, refonte mobile-responsive (≤768px)**. (1) **Drawer sommaire** : `#config-nav` en panneau `position: fixed` (`min(320px, 88vw)`, z-index 40) sous backdrop `#config-modal.config-toc-open::before` (z-index 35) ; bouton `#config-toc-close` ; backdrop/Échap ferment le drawer d'abord puis la modale ; `_setConfigNav` bascule la classe + conserve le `display` inline de reset. (2) **Modale plein écran** : `100dvh` + `padding: 0`, `border-radius: 0`. (3) **Tactile** : boutons/liens ≥44px, inputs/selects `16px` + `min-height: 44px` (anti-zoom iOS, scopé `#config-modal`), rangée `.config-actions-row` sticky column + safe-area, formulaires 1 colonne, MFA 1 colonne + code full-width, wrap webhook/token/share/diag/avatar/webauthn. (4) **Dettes HTML/i18n** : `.config-actions-row` replacée dans `#cfg-backend-settings` (`</section>` orphelin supprimé), id dupliqué `cfg-partages-publics` retiré du `<h2>`, `#plugins-settings-container` supprimé, doublons `.config-btn-add` + règle morte `.mfa-recovery-input` purgés, `#mt-explorer` → `data-i18n="settings.explorer"` ; i18n : clés mortes `settings.{backend,backend_hint,restart_badge,save,plugins}` supprimées, `settings.explorer` + `config.toc_close` ajoutées, `settings.tabs` FR = « Onglets ». Vérifié : `config-mobile.test.mjs` 27/27 (au CI), unit 11/11, validate-imports 40 modules, pytest 1302 passed / 6 skipped, ruff/mypy 0, E2E `chromium-mobile` 5/5. | ✅ livré (en attente vérif utilisateur) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 2.22.1 | **Dernière mise à jour :** 2026-09-23
|
||||
> **Version :** 2.23.0 | **Dernière mise à jour :** 2026-09-24
|
||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||
> vers les fonctionnalités livrées.
|
||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||
@@ -40,6 +40,8 @@
|
||||
- [ ] **Signature de code Windows** : non retenue (pas de certificat) — alternatives : livrer non signé, SignPath.io (OSS gratuit), Certum OSS, Azure Trusted Signing, certificat EV
|
||||
- [ ] Exécuter les 6 tests E2E **manuels** — protocole documenté : [DESKTOP_E2E_CHECKLIST.md](./DESKTOP_E2E_CHECKLIST.md)
|
||||
|
||||
---
|
||||
|
||||
## ⚪ Backlog — Priorité 4 (P4)
|
||||
|
||||
### 73. Synchronisation multi-appareils — Obsidian Sync compatible
|
||||
@@ -186,6 +188,7 @@
|
||||
| 111 | Visionneuse d'images — navigation fluide : image ajustée au cadre, navigation en place (cache annuaire + préchargement), pellicule persistante, flèches latérales au survol | 2.20.0 | [features/image-navigation-111.md](./features/image-navigation-111.md) |
|
||||
| 112 | En-tête allégé & compte en sidebar — version dans le menu Options, retrait utilisateur/déconnexion du header, section compte en bas de la sidebar, pellicule d'images défilable (molette + flèches) | 2.21.0 | [features/header-user-sidebar-112.md](./features/header-user-sidebar-112.md) |
|
||||
| 113 | Configuration — ordre naturel des sections (Profil 1er, À propos dernier, TOC = page) & avatar utilisateur (import PNG/JPG/WEBP, persistance serveur, cercle sidebar) | 2.22.0 | [features/settings-order-avatar-113.md](./features/settings-order-avatar-113.md) |
|
||||
| 114 | Configuration — refonte mobile-responsive (modale plein écran 100dvh, sommaire en drawer coulissant, cibles tactiles ≥ 44 px, inputs 16 px anti-zoom, sauvegarde sticky, MFA 1 colonne, purge i18n/HTML) | 2.23.0 | [features/settings-mobile-114.md](./features/settings-mobile-114.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -193,7 +196,7 @@
|
||||
|
||||
| Priorité | Items | Effort total estimé |
|
||||
|---|---|---|
|
||||
| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–113, #92 | ~131 jours réalisés |
|
||||
| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–114, #92 | ~133 jours réalisés |
|
||||
| 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour |
|
||||
| ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours |
|
||||
| ⚪ P0/P1 restant | #85, #87 Refonte architecturale, CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~11-17 jours |
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# #114 — Configuration — refonte mobile-responsive de la section Settings
|
||||
|
||||
> **Statut :** 🟢 · **Impact :** 🟡 · **Zone :** frontend (mobile, ≤ 768 px)
|
||||
> **Fichiers :** `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`,
|
||||
> `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs`,
|
||||
> `tests/e2e/config-mobile.spec.js`.
|
||||
|
||||
## Contexte
|
||||
|
||||
La page **Configurations** (`#config-modal`) était utilisable en mobile seulement
|
||||
partiellement (correctifs BUG-071) : le sommaire s'ouvrait en bloc haut, la modale
|
||||
n'était pas plein écran, les cibles tactiles étaient sous 44 px, le clavier virtuel
|
||||
iOS zoomait les champs, la rangée « Sauvegarder » disparaissait au scroll et plusieurs
|
||||
dettes HTML/i18n étaient restées en place.
|
||||
|
||||
## Ce qui a été livré
|
||||
|
||||
### A. Sommaire en drawer coulissant
|
||||
|
||||
- `#config-nav` devient un **panneau coulissant gauche** (`position: fixed`,
|
||||
`width: min(320px, 88vw)`, `z-index: 40`) sous un fond assombri
|
||||
(`#config-modal.config-toc-open::before`, `z-index: 35`).
|
||||
- Ouverture/fermeture pilotée par la classe **`.config-toc-open`** sur `#config-modal`
|
||||
(JS `_setConfigNav`) + un `display` inline conservé pour le contrat de reset.
|
||||
- Bouton **`#config-toc-close`** (classe `.help-toc-close`, `aria-label`
|
||||
`config.toc_close`) dans l'en-tête du drawer ; visible uniquement dans le drawer
|
||||
de la config (masqué sur desktop où la nav est toujours visible).
|
||||
- **Backdrop** : un tap hors du drawer ferme d'abord le drawer, pas la modale
|
||||
(`e.target === modal` → `config-toc-open` présent → `_setConfigNav(false)`).
|
||||
- **Échap** : ferme le drawer d'abord, puis la modale.
|
||||
- Fermeture de la modale (`closeConfigModal`) nettoie toujours la classe
|
||||
`config-toc-open` et le `display` inline (aucun fond ne subsiste).
|
||||
- Animation `config-toc-slide-in` (translateX) à l'ouverture.
|
||||
|
||||
### B. Modale plein écran
|
||||
|
||||
- `#config-modal` : `padding: 0`, `.editor-container` en `100vw × 100dvh`
|
||||
(`100dvh` = hauteur du viewport dynamique, tient compte de la barre du navigateur
|
||||
mobile), `border-radius: 0`, `border: none`.
|
||||
- Le contenu (`#config-scroll`) garde son scroll propre ; le sous-bloc
|
||||
`.config-content` reçoit un `padding-bottom: 80 px` pour ne jamais passer sous la
|
||||
rangée sticky.
|
||||
|
||||
### C. Cibles tactiles ≥ 44 px & anti-zoom iOS
|
||||
|
||||
- **Champs** : `.config-input`, `.config-select`, `.help-nav-search`,
|
||||
`.profile-field .config-input/.config-select` → `min-height: 44px` +
|
||||
`font-size: 16px` (**anti-zoom iOS** : un `font-size < 16px` déclenche le zoom
|
||||
automatique au focus). La règle est **scoppée `#config-modal`** pour ne pas écraser
|
||||
`.mfa-code-input` (qui a sa propre typographie).
|
||||
- **Boutons** : `.config-btn-save`, `.config-btn-secondary`, `.config-btn-primary`,
|
||||
`.config-btn-danger`, `.config-btn-add`, `.config-btn-sm`, `.mfa-link-btn`,
|
||||
`.theme-action-btn`, `.profile-avatar-actions .config-btn-secondary`,
|
||||
`.editor-btn` (fermer), `#config-hamburger`, `#config-toc-close`,
|
||||
`.help-search-clear` → `min-height/min-width: 44px`.
|
||||
- **Liens du sommaire** : `.help-nav-link` → `min-height: 44px` (ligne tactile
|
||||
confortable).
|
||||
- `.help-hamburger` passe de 36 px à **44 px** en mobile (règle partagée avec le
|
||||
modal d'aide).
|
||||
|
||||
### D. Rangée « Sauvegarder » sticky
|
||||
|
||||
- `.config-actions-row` (dans `#cfg-backend-settings`) → `position: sticky;
|
||||
bottom: 0`, empilée verticalement (`flex-direction: column`), boutons pleine
|
||||
largeur 44 px, fond opaque + bordure, `padding-bottom` avec
|
||||
`env(safe-area-inset-bottom)` (barre home iOS).
|
||||
- La rangée reste visible pendant le scroll de la section backend ; le
|
||||
`padding-bottom: 80px` de `.config-content` garantit qu'elle ne masque jamais les
|
||||
derniers contrôles.
|
||||
|
||||
### E. Formulaires 1 colonne & grilles
|
||||
|
||||
- `.config-row` → 1 colonne (`grid-template-columns: 1fr`), `.config-input--num`
|
||||
pleine largeur, `text-align: left`.
|
||||
- `.ai-default-grid` / `.ai-provider-fields` → 1 colonne.
|
||||
- Add-rows (`.config-add-row`, `.config-add-pattern`) → wrap + largeurs inline
|
||||
(`180/140/100px`) neutralisées (`width: auto !important`).
|
||||
- Items webhook/token/share → wrap ; URLs/méta sur leur propre ligne
|
||||
(`overflow-wrap: anywhere`) ; boutons de suppression 44 px.
|
||||
- `.hidden-files-add-row` → wrap, input pleine largeur.
|
||||
- `.config-diag-row` → wrap.
|
||||
- `.profile-avatar-row` → wrap ; `.profile-form` → `max-width: 100%`.
|
||||
- `.webauthn-key-item` → wrap ; `.webauthn-key-label` → pleine largeur.
|
||||
- `.theme-grid` reste en `auto-fill minmax(160px, 1fr)` (déjà responsive).
|
||||
|
||||
### F. MFA & sécurité
|
||||
|
||||
- `.mfa-recovery-list` → **1 colonne** en mobile (2 colonnes illisibles à 360 px).
|
||||
- `.mfa-verify-section`, `.mfa-recovery-actions`, `.mfa-disable-actions` → wrap ;
|
||||
champs/boutons enfants en pleine largeur.
|
||||
- `.mfa-code-input` → `width: 100%`, `max-width: 320px`, `letter-spacing: 6px`
|
||||
(au lieu de 12 px qui débordait), `min-height: 52px`.
|
||||
- `.mfa-secret-code` → `word-break: break-all` (secret TOTP long).
|
||||
- `.mfa-code-input-group` → wrap.
|
||||
- Règle morte **`.mfa-recovery-input`** supprimée (auth.js utilise
|
||||
`mfa-code-input recovery-input`).
|
||||
|
||||
### G. Dettes HTML corrigées
|
||||
|
||||
- `.config-actions-row` (Sauvegarder / Réindexer / Réinitialiser) déplacée
|
||||
**dans** `#cfg-backend-settings` (elle était hors de toute section → le sticky
|
||||
n'avait pas de conteneur de scroll fiable) ; `</section>` orphelin supprimé.
|
||||
- Id dupliqué **`cfg-partages-publics`** retiré du `<h2>` (l'id reste sur la
|
||||
`<section>`, cf. #113).
|
||||
- Conteneur mort **`#plugins-settings-container`** supprimé (le rendu réel est
|
||||
`#cfg-plugins` via `plugins.js`).
|
||||
- Sections plugins/about ré-indentées.
|
||||
- **`#mt-explorer`** : libellé brut `settings.search` remplacé par
|
||||
`<span data-i18n="settings.explorer">` (i18n correct, FR « Explorateur » / EN
|
||||
« Files »).
|
||||
- Doublons CSS **`.config-btn-add`** (3 définitions) réduits à la définition de
|
||||
référence.
|
||||
|
||||
### H. i18n FR/EN
|
||||
|
||||
- **Purge des clés mortes** (aucune référence HTML/JS/tests/backend) :
|
||||
`settings.backend`, `settings.backend_hint`, `settings.restart_badge`,
|
||||
`settings.save`, `settings.plugins`.
|
||||
- **Ajouts** : `settings.explorer` (FR « Explorateur » / EN « Files »),
|
||||
`config.toc_close` (FR « Fermer le sommaire » / EN « Close contents »).
|
||||
- **Correctif** : `settings.tabs` en FR était le mot anglais « Tabs » →
|
||||
**« Onglets »** (EN reste « Tabs »).
|
||||
- Clés vivantes conservées : `settings.reindex`, `settings.no_restart_badge`,
|
||||
`settings.backend_section`, `settings.security`, `settings.search`,
|
||||
`settings.tabs`, `settings.search_placeholder`.
|
||||
|
||||
## Tests
|
||||
|
||||
### Statics — `tests/frontend/config-mobile.test.mjs` (27, au CI)
|
||||
|
||||
- BUG-071a–e (hamburger, toggle JS, grilles/wrap, ancres mortes,
|
||||
`data-i18n-attr` multi-paires) — conservés et adaptés au drawer.
|
||||
- **#114a** : `#config-toc-close` présent + i18n ; `_setConfigNav` bascule
|
||||
`.config-toc-open` ; backdrop `::before` (z-index 35) ; `.help-toc-close`
|
||||
masqué sur desktop / visible dans le drawer ; Échap et backdrop ferment le
|
||||
drawer d'abord ; `closeConfigModal` nettoie la classe.
|
||||
- **#114b** : modale `100dvh` + `padding: 0` ; inputs/selects `16px` +
|
||||
`44px` ; boutons `44px` ; rangée sticky (`position: sticky` +
|
||||
`flex-direction: column` + safe-area) ; MFA 1 colonne + wrap + code
|
||||
full-width ; `.config-actions-row` bien dans `#cfg-backend-settings`.
|
||||
- **#114i18n** : clés mortes purgées ; `settings.explorer` FR/EN ;
|
||||
`settings.tabs` FR = « Onglets » ; `#mt-explorer` porte
|
||||
`data-i18n="settings.explorer"` ; `#plugins-settings-container` absent.
|
||||
|
||||
### E2E — `tests/e2e/config-mobile.spec.js` (5, projet `chromium-mobile`)
|
||||
|
||||
1. Hamburger → drawer `position: fixed` + classe `config-toc-open` sur la modale.
|
||||
2. Bouton `#config-toc-close` et tap backdrop ferment le drawer **sans** fermer la
|
||||
modale.
|
||||
3. Sélection d'une section → scroll doux + lien actif + repli du drawer.
|
||||
4. Modale plein écran (largeur/hauteur ≈ viewport) + `#config-hamburger`,
|
||||
`#config-close` et `#cfg-save-backend` ≥ 44 px.
|
||||
5. Aucun débordement horizontal à 393 px (sections IA / tokens / webhooks /
|
||||
partages).
|
||||
|
||||
## Détails d'implémentation notables
|
||||
|
||||
- **Spécificité CSS** : les règles `#config-modal #config-nav` (2 ids) priment sur
|
||||
les règles génériques `.help-nav` / `body .help-nav` qui masquent la nav en
|
||||
mobile — pas besoin de `!important`.
|
||||
- **Backdrop = pseudo-élément** : les clics sur `::before` sont attribués à
|
||||
l'élément or (`#config-modal`), donc le handler `e.target === modal` existant
|
||||
fonctionne sans node supplémentaire.
|
||||
- **Contrat de reset conservé** : `configNavOnOpen.style.display = ''` à
|
||||
l'ouverture (test statique BUG-071b) — le CSS reprend la main (drawer masqué par
|
||||
défaut sur mobile).
|
||||
- **`100dvh` avec repli `100vh`** : les navigateurs sans support `dvh` gardent le
|
||||
comportement précédent.
|
||||
- La règle `.help-hamburger { display: inline-flex }` du bloc mobile du modal
|
||||
d'aide est **partagée** (44 px) ; le drawer de la config double avec
|
||||
`#config-modal .help-hamburger` pour rester robuste à un réordonnancement des
|
||||
règles.
|
||||
+28
-19
@@ -1551,6 +1551,17 @@
|
||||
<nav class="help-nav" id="config-nav">
|
||||
<div class="help-nav-header">
|
||||
<div class="help-nav-title" data-i18n="config.title">Configuration</div>
|
||||
<button
|
||||
class="help-toc-close"
|
||||
id="config-toc-close"
|
||||
data-i18n-attr="aria-label:config.toc_close"
|
||||
aria-label="Fermer le sommaire"
|
||||
>
|
||||
<i
|
||||
data-lucide="x"
|
||||
style="width: 16px; height: 16px"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-nav-search-wrap">
|
||||
<input
|
||||
@@ -2146,7 +2157,6 @@
|
||||
>Nombre max de tokens élargis par préfixe (10-200)</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<div class="config-actions-row">
|
||||
<button class="config-btn-save"
|
||||
id="cfg-save-backend" data-i18n="help.shortcut_save">
|
||||
@@ -2509,7 +2519,7 @@
|
||||
|
||||
<!-- Partages publics -->
|
||||
<section class="config-section help-section" id="cfg-partages-publics">
|
||||
<h2 id="cfg-partages-publics">📤 Partages publics</h2>
|
||||
<h2>📤 Partages publics</h2>
|
||||
<p class="config-description">
|
||||
Liens de partage publics pour des documents
|
||||
(lecture seule, sans authentification).
|
||||
@@ -2517,23 +2527,22 @@
|
||||
<div id="shares-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- Plugins -->
|
||||
<section
|
||||
class="config-section help-section"
|
||||
id="cfg-plugins"
|
||||
>
|
||||
<h2 data-i18n="config.section_plugins">🧩 Plugins</h2>
|
||||
<p class="config-description" data-i18n="plugins.description">
|
||||
Extend ObsiGate with custom renderers, search filters, and editor actions.
|
||||
</p>
|
||||
<div id="plugins-settings-container"></div>
|
||||
</section>
|
||||
<!-- Plugins -->
|
||||
<section
|
||||
class="config-section help-section"
|
||||
id="cfg-plugins"
|
||||
>
|
||||
<h2 data-i18n="config.section_plugins">🧩 Plugins</h2>
|
||||
<p class="config-description" data-i18n="plugins.description">
|
||||
Extend ObsiGate with custom renderers, search filters, and editor actions.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- À propos -->
|
||||
<section
|
||||
class="config-section help-section"
|
||||
id="cfg-about"
|
||||
>
|
||||
<!-- À propos -->
|
||||
<section
|
||||
class="config-section help-section"
|
||||
id="cfg-about"
|
||||
>
|
||||
<h2 data-i18n="auto.a3319169">📦 À propos</h2>
|
||||
<div
|
||||
id="config-about"
|
||||
@@ -5614,7 +5623,7 @@ curl -X POST https://votre-serveur.com/webhook \
|
||||
data-lucide="folder-open"
|
||||
style="width: 20px; height: 20px"
|
||||
></i>
|
||||
<span class="mt-label">settings.search</span>
|
||||
<span class="mt-label" data-i18n="settings.explorer">Explorateur</span>
|
||||
</button>
|
||||
<button
|
||||
class="mt-btn"
|
||||
|
||||
+38
-6
@@ -767,10 +767,15 @@ function initConfigModal() {
|
||||
openBtn.addEventListener("click", async () => {
|
||||
modal.classList.add("active");
|
||||
closeHeaderMenu();
|
||||
// BUG-071: reset the TOC to the CSS default (mobile: hidden, desktop:
|
||||
// visible) like the help modal does on open.
|
||||
// BUG-071/#114: reset the TOC to the CSS default (mobile: hidden drawer,
|
||||
// desktop: visible sidebar) like the help modal does on open. Clear the
|
||||
// stale inline display and the drawer-open class so a previous mobile
|
||||
// session cannot leave the nav stuck open.
|
||||
var configNavOnOpen = document.getElementById("config-nav");
|
||||
if (configNavOnOpen) configNavOnOpen.style.display = '';
|
||||
modal.classList.remove("config-toc-open");
|
||||
var configHamburgerOnOpen = document.getElementById("config-hamburger");
|
||||
if (configHamburgerOnOpen) configHamburgerOnOpen.classList.remove("active");
|
||||
renderConfigFilters();
|
||||
loadConfigFields();
|
||||
loadDiagnostics();
|
||||
@@ -786,6 +791,12 @@ function initConfigModal() {
|
||||
closeBtn.addEventListener("click", closeConfigModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
// #114: a tap on the backdrop (or outside the drawer) first closes the
|
||||
// TOC drawer; only a second tap closes the whole modal.
|
||||
if (modal.classList.contains("config-toc-open")) {
|
||||
_setConfigNav(false);
|
||||
return;
|
||||
}
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
@@ -890,17 +901,19 @@ function initConfigModal() {
|
||||
});
|
||||
}
|
||||
|
||||
// BUG-071: mobile table of contents. #config-nav shares the .help-nav
|
||||
// BUG-071/#114: mobile table of contents. #config-nav shares the .help-nav
|
||||
// rule that hides it below 768px, but — unlike the help modal — the config
|
||||
// modal had no toggle to reveal it, leaving mobile users with no way to
|
||||
// reach a section. The header hamburger opens it as a top block; picking
|
||||
// a section smooth-scrolls inside the modal and collapses it on mobile.
|
||||
// reach a section. The header hamburger opens it as a left slide-over
|
||||
// drawer (backdrop via .config-toc-open on the modal); picking a section
|
||||
// smooth-scrolls inside the modal and collapses it on mobile.
|
||||
var configNav = document.getElementById("config-nav");
|
||||
var configHamburger = document.getElementById("config-hamburger");
|
||||
function _isConfigMobile() { return window.innerWidth <= 768; }
|
||||
function _setConfigNav(open) {
|
||||
if (!configNav) return;
|
||||
configNav.style.display = open ? "flex" : "none";
|
||||
modal.classList.toggle("config-toc-open", !!open && _isConfigMobile());
|
||||
if (configHamburger) configHamburger.classList.toggle("active", !!open);
|
||||
}
|
||||
if (configHamburger) {
|
||||
@@ -910,6 +923,14 @@ function initConfigModal() {
|
||||
_setConfigNav(hidden);
|
||||
});
|
||||
}
|
||||
// #114: close button inside the drawer header.
|
||||
var configTocClose = document.getElementById("config-toc-close");
|
||||
if (configTocClose) {
|
||||
configTocClose.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
_setConfigNav(false);
|
||||
});
|
||||
}
|
||||
if (configNav) {
|
||||
configNav.querySelectorAll(".help-nav-link").forEach(function(a) {
|
||||
a.addEventListener("click", function(e) {
|
||||
@@ -930,6 +951,11 @@ function initConfigModal() {
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||||
// #114: Escape closes the TOC drawer first, then the modal.
|
||||
if (modal.classList.contains("config-toc-open")) {
|
||||
_setConfigNav(false);
|
||||
return;
|
||||
}
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
@@ -949,7 +975,13 @@ function initConfigModal() {
|
||||
|
||||
function closeConfigModal() {
|
||||
const modal = document.getElementById("config-modal");
|
||||
if (modal) modal.classList.remove("active");
|
||||
if (modal) {
|
||||
modal.classList.remove("active");
|
||||
// #114: never leave the drawer-open class (backdrop) behind.
|
||||
modal.classList.remove("config-toc-open");
|
||||
const nav = document.getElementById("config-nav");
|
||||
if (nav) nav.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Config field helpers ---
|
||||
|
||||
@@ -578,6 +578,7 @@
|
||||
"config.test": "Test",
|
||||
"config.timeout_label": "Search timeout (ms)",
|
||||
"config.title": "Settings",
|
||||
"config.toc_close": "Close contents",
|
||||
"config.toc_toggle": "Show contents",
|
||||
"config.title_boost": "Title boost",
|
||||
"config.title_boost_hint": "Relevance multiplier for title matches",
|
||||
@@ -1631,25 +1632,21 @@
|
||||
"search.whole_word": "Whole word",
|
||||
"settings.about": "📦 About",
|
||||
"settings.ai": "🤖 AI",
|
||||
"settings.backend": "⚙️ Backend",
|
||||
"settings.backend_hint": "These settings are saved on the server. Some require a restart or reindexing.",
|
||||
"settings.backend_section": "Backend Settings",
|
||||
"settings.client_label": "These settings apply immediately on the client side.",
|
||||
"settings.diagnostics": "🩺 Diagnostics",
|
||||
"settings.explorer": "Files",
|
||||
"settings.history_count_desc": "Between 5 and 100 files (saved on server)",
|
||||
"settings.history_count_label": "Files in history",
|
||||
"settings.history_section": "Recent History",
|
||||
"settings.no_restart_badge": "No restart needed",
|
||||
"settings.profile": "👤 Profile",
|
||||
"settings.reindex": "Force reindex",
|
||||
"settings.restart_badge": "Restart required",
|
||||
"settings.save": "Save",
|
||||
"settings.search": "Search",
|
||||
"settings.tabs": "Tabs",
|
||||
"settings.search_placeholder": "Search...",
|
||||
"settings.sync": "🔄 Sync",
|
||||
"settings.tabs": "Tabs",
|
||||
"settings.themes": "🎨 Themes",
|
||||
"settings.plugins": "🧩 Plugins",
|
||||
"share.copied": "Link copied!",
|
||||
"share.copy_link": "Copy link",
|
||||
"share.create": "Create share link",
|
||||
|
||||
@@ -578,6 +578,7 @@
|
||||
"config.test": "Tester",
|
||||
"config.timeout_label": "Timeout recherche (ms)",
|
||||
"config.title": "Configuration",
|
||||
"config.toc_close": "Fermer le sommaire",
|
||||
"config.toc_toggle": "Afficher le sommaire",
|
||||
"config.title_boost": "Boost titre",
|
||||
"config.title_boost_hint": "Multiplicateur de pertinence pour les correspondances dans le titre",
|
||||
@@ -1631,25 +1632,21 @@
|
||||
"search.whole_word": "Mot entier",
|
||||
"settings.about": "📦 À propos",
|
||||
"settings.ai": "🤖 IA",
|
||||
"settings.backend": "⚙️ Backend",
|
||||
"settings.backend_hint": "Ces paramètres sont sauvegardés sur le serveur. Certains nécessitent un redémarrage ou une réindexation.",
|
||||
"settings.backend_section": "Paramètres backend",
|
||||
"settings.client_label": "Ces paramètres s'appliquent immédiatement côté client.",
|
||||
"settings.diagnostics": "🩺 Diagnostics",
|
||||
"settings.explorer": "Explorateur",
|
||||
"settings.history_count_desc": "Entre 5 et 100 fichiers (sauvegarde sur le serveur)",
|
||||
"settings.history_count_label": "Nombre de fichiers dans l'historique",
|
||||
"settings.history_section": "Historique récent",
|
||||
"settings.no_restart_badge": "Redémarrage non requis",
|
||||
"settings.profile": "👤 Profil",
|
||||
"settings.reindex": "Forcer réindexation",
|
||||
"settings.restart_badge": "Redémarrage requis",
|
||||
"settings.save": "Sauvegarder",
|
||||
"settings.search": "Recherche",
|
||||
"settings.tabs": "Tabs",
|
||||
"settings.search_placeholder": "Rechercher...",
|
||||
"settings.sync": "🔄 Synchronisation",
|
||||
"settings.tabs": "Onglets",
|
||||
"settings.themes": "🎨 Thèmes",
|
||||
"settings.plugins": "🧩 Plugins",
|
||||
"share.copied": "Lien copié !",
|
||||
"share.copy_link": "Copier le lien",
|
||||
"share.create": "Créer un lien de partage",
|
||||
|
||||
+272
-38
@@ -3164,6 +3164,23 @@ select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.help-hamburger:hover { color: var(--accent); border-color: var(--accent); }
|
||||
/* #114: close button for the mobile TOC drawer (hidden on desktop where the
|
||||
nav is always visible). Scoped to #config-modal so the help modal — which
|
||||
has no such button — is unaffected. */
|
||||
.help-toc-close {
|
||||
display: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.help-nav {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
@@ -4777,22 +4794,204 @@ body.resizing-v {
|
||||
}
|
||||
}
|
||||
|
||||
/* BUG-071: Configurations modal — mobile usability (viewport ≤ 768px).
|
||||
#config-nav shares the .help-nav rule that hides it, but the config modal
|
||||
had no toggle (unlike the help modal): the header hamburger
|
||||
(#config-hamburger, same .help-hamburger treatment) reveals it as a
|
||||
collapsible top block. Two-column grids and fixed-width add-rows are
|
||||
stacked/wrapped so nothing overflows a 360px viewport. */
|
||||
/* --- #114: Configurations modal — mobile UX refonte (viewport ≤ 768px).
|
||||
Builds on BUG-071 (which stacked grids/wrapped rows): the TOC becomes a
|
||||
left slide-over drawer with a backdrop, the modal goes full-screen with
|
||||
100dvh, every interactive control gets a ≥44px touch target, inputs are
|
||||
≥16px to stop iOS auto-zoom, and the backend Save row sticks to the
|
||||
bottom of its scroll container. The drawer is opened by JS toggling
|
||||
.config-toc-open on #config-modal (backdrop + nav visibility) plus an
|
||||
inline display on #config-nav (kept for the display-reset contract). */
|
||||
@media (max-width: 768px) {
|
||||
/* TOC as a collapsible top block (JS toggles inline display flex/none,
|
||||
which wins over the hiding rule); the list scrolls within a capped nav. */
|
||||
#config-modal #config-nav {
|
||||
/* Full-screen modal with dynamic viewport height (mobile browser chrome). */
|
||||
#config-modal {
|
||||
padding: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
#config-modal .editor-container {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Hamburger: 44px touch target (shared .help-hamburger rule in the help
|
||||
modal media query also sets 44px; this scopes it to the config modal
|
||||
in case rule order changes). */
|
||||
#config-modal .help-hamburger {
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
/* Close button in the TOC drawer header. */
|
||||
#config-modal .help-toc-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#config-modal .help-nav-header {
|
||||
padding: 12px 12px 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
/* Close (X) in the modal header. */
|
||||
#config-modal .editor-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
/* Anti-zoom: ≥16px font on text fields inside the config modal. Never
|
||||
target input[type=text] globally — it would clobber .mfa-code-input. */
|
||||
#config-modal .config-input,
|
||||
#config-modal .config-select,
|
||||
#config-modal .help-nav-search,
|
||||
#config-modal .profile-field .config-input,
|
||||
#config-modal .profile-field .config-select {
|
||||
min-height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
#config-modal .config-input--num {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
#config-modal .help-nav-search-wrap {
|
||||
padding: 0 12px 10px;
|
||||
}
|
||||
#config-modal .help-search-clear {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
#config-modal .help-nav-link {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Sticky Save row inside #cfg-backend-settings (its scroll container is
|
||||
#config-scroll.help-content, which is the overflow ancestor). */
|
||||
#config-modal .config-actions-row {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0));
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
#config-modal .config-actions-row .config-btn-save,
|
||||
#config-modal .config-actions-row .config-btn-secondary,
|
||||
#config-modal .config-actions-row .config-btn-primary,
|
||||
#config-modal .config-actions-row .config-btn-danger {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#config-modal .config-btn-save,
|
||||
#config-modal .config-btn-secondary,
|
||||
#config-modal .config-btn-primary,
|
||||
#config-modal .config-btn-danger,
|
||||
#config-modal .config-btn-add,
|
||||
#config-modal .config-btn-sm,
|
||||
#config-modal .mfa-link-btn,
|
||||
#config-modal .theme-action-btn,
|
||||
#config-modal .profile-avatar-actions .config-btn-secondary,
|
||||
#config-modal .editor-btn {
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#config-modal .config-btn-sm,
|
||||
#config-modal .mfa-link-btn,
|
||||
#config-modal .theme-action-btn {
|
||||
min-width: 44px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
/* Profile avatar row: wrap instead of overflowing a 360px viewport. */
|
||||
#config-modal .profile-avatar-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
#config-modal .profile-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
/* MFA: one-column recovery list, wrapping action rows, full-width code
|
||||
input with a readable letter-spacing (12px overflows 360px). */
|
||||
#config-modal .mfa-recovery-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
#config-modal .mfa-verify-section,
|
||||
#config-modal .mfa-recovery-actions,
|
||||
#config-modal .mfa-disable-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#config-modal .mfa-verify-section .config-input,
|
||||
#config-modal .mfa-verify-section .config-btn-primary {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
max-height: 46vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#config-modal .mfa-recovery-actions .config-btn-secondary,
|
||||
#config-modal .mfa-disable-actions .config-btn-danger {
|
||||
flex: 1 1 auto;
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#config-modal .mfa-code-input {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
min-height: 52px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
#config-modal .mfa-secret-code {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
#config-modal .mfa-code-input-group {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
/* WebAuthn key rows: wrap instead of clipping the remove button. */
|
||||
#config-modal .webauthn-key-item {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
#config-modal .webauthn-key-label {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
#config-modal .webauthn-key-item .config-btn-sm {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
#config-modal .hidden-files-add-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#config-modal .hidden-files-add-row .config-input {
|
||||
flex: 1 1 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
}
|
||||
#config-modal .config-diag-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
}
|
||||
/* AI keys sticky footer: clear the mobile toolbar + home indicator. */
|
||||
#config-modal .ai-keys-footer {
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0));
|
||||
bottom: 0;
|
||||
}
|
||||
/* Two-column grids → single column. */
|
||||
#config-modal .ai-default-grid,
|
||||
@@ -4851,6 +5050,61 @@ body.resizing-v {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* Drawer: left slide-over above a dimming backdrop. Higher specificity
|
||||
than the generic .help-nav rules (body .help-nav / .help-nav) that hide
|
||||
the nav on mobile. JS toggles .config-toc-open on #config-modal. */
|
||||
#config-modal #config-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: min(320px, 88vw);
|
||||
min-width: 0;
|
||||
max-width: 88vw;
|
||||
max-height: 100dvh;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 40;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
animation: config-toc-slide-in 200ms ease-out;
|
||||
}
|
||||
#config-modal.config-toc-open #config-nav {
|
||||
display: flex;
|
||||
max-height: 100dvh;
|
||||
}
|
||||
#config-modal .help-nav-list {
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
/* Backdrop: covers the full modal, sits above the content (z-index auto)
|
||||
and below the drawer (z-index 40). Clicks on the pseudo-element are
|
||||
attributed to #config-modal, so the existing e.target === modal handler
|
||||
closes the drawer first; it also blocks taps reaching the content. */
|
||||
#config-modal.config-toc-open::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 35;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
@keyframes config-toc-slide-in {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
/* Content area gets a bottom pad so the sticky Save row never covers the
|
||||
last controls while the virtual keyboard is open. */
|
||||
#config-modal #config-scroll .config-content {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Toast notifications --- */
|
||||
@@ -5819,28 +6073,6 @@ body.resizing-v {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.config-btn-add {
|
||||
padding: 6px 12px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-btn-add:hover {
|
||||
background: color-mix(in srgb, var(--accent) 85%, black);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.config-btn-add:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Utility — hidden class
|
||||
--------------------------------------------------------------------------- */
|
||||
@@ -7907,7 +8139,6 @@ body.popup-mode .content-area {
|
||||
.webhook-events { color: var(--text-muted); font-size: 0.7rem; }
|
||||
.webhook-delete { background: none; border: none; color: var(--text-error); cursor: pointer; font-size: 1rem; padding: 2px 6px; }
|
||||
.config-add-row { display: flex; gap: 8px; margin-top: 8px; }
|
||||
.config-btn-add { padding: 6px 14px; background: var(--accent); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.8rem; }
|
||||
|
||||
/* ── API/MCP tokens UI (#107) ── */
|
||||
.token-item {
|
||||
@@ -8506,9 +8737,13 @@ body.desktop-mode .editor-container {
|
||||
|
||||
.help-hamburger {
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* TOC: collapsible block at top (not slide-over) */
|
||||
/* TOC drawer: collapsible block for the help modal; the config modal
|
||||
(#114) overrides this with a fixed left drawer + backdrop. */
|
||||
.help-nav {
|
||||
position: static;
|
||||
width: 100%;
|
||||
@@ -10763,7 +10998,6 @@ body.desktop-mode .editor-container {
|
||||
}
|
||||
.mfa-link-btn:hover { opacity: 0.8; }
|
||||
.mfa-error { color: #e74c3c; font-size: 13px; margin-top: 8px; }
|
||||
.mfa-recovery-input { width: 200px; font-size: 20px; letter-spacing: 4px; }
|
||||
|
||||
/* MFA Settings (Security tab) */
|
||||
.mfa-status-section { padding: 16px 0; }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsigate",
|
||||
"version": "2.22.1",
|
||||
"version": "2.23.0",
|
||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||
"main": "patch.js",
|
||||
"directories": {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* E2E tests for the Configurations modal on mobile (BUG-071).
|
||||
* E2E tests for the Configurations modal on mobile (BUG-071 + #114).
|
||||
*
|
||||
* Runs only under the `chromium-mobile` Playwright project (viewport ≤ 768px);
|
||||
* skipped on the desktop project that the CI job executes — same convention
|
||||
* as mobile-editor.spec.js.
|
||||
*
|
||||
* Covered:
|
||||
* - the TOC hamburger (#config-hamburger) is visible and reveals #config-nav,
|
||||
* which is hidden by default on mobile;
|
||||
* - the TOC hamburger (#config-hamburger) is visible and reveals #config-nav
|
||||
* as a left slide-over drawer (fixed positioning), hidden by default;
|
||||
* - the drawer has a close button (#config-toc-close) and a dimming backdrop
|
||||
* (tap outside closes the drawer, not the whole modal);
|
||||
* - picking a TOC entry scrolls to the section, marks the link active and
|
||||
* collapses the nav;
|
||||
* collapses the drawer;
|
||||
* - the modal is full-screen (100dvh) and interactive controls are ≥44px;
|
||||
* - the modal content does not overflow horizontally at 393px.
|
||||
*
|
||||
* Run:
|
||||
@@ -20,6 +23,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const MOBILE_MAX_WIDTH = 768;
|
||||
const MIN_TOUCH_TARGET = 44;
|
||||
|
||||
async function boot(page) {
|
||||
await page.goto('/');
|
||||
@@ -33,8 +37,8 @@ async function openConfigModal(page) {
|
||||
await expect(page.locator('#config-modal.active')).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe('Configurations modal on mobile (BUG-071)', () => {
|
||||
test('hamburger reveals the table of contents', async ({ page, viewport }) => {
|
||||
test.describe('Configurations modal on mobile (BUG-071 + #114)', () => {
|
||||
test('hamburger reveals the TOC as a fixed drawer', async ({ page, viewport }) => {
|
||||
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
||||
await boot(page);
|
||||
await openConfigModal(page);
|
||||
@@ -45,9 +49,39 @@ test.describe('Configurations modal on mobile (BUG-071)', () => {
|
||||
|
||||
await page.locator('#config-hamburger').click();
|
||||
await expect(page.locator('#config-nav')).toBeVisible();
|
||||
|
||||
// #114: the drawer is a fixed slide-over, not an inline top block.
|
||||
const position = await page.locator('#config-nav').evaluate(
|
||||
(el) => window.getComputedStyle(el).position,
|
||||
);
|
||||
expect(position).toBe('fixed');
|
||||
|
||||
// The modal carries the drawer-open class (backdrop contract).
|
||||
await expect(page.locator('#config-modal')).toHaveClass(/config-toc-open/);
|
||||
});
|
||||
|
||||
test('picking a section scrolls to it and collapses the nav', async ({ page, viewport }) => {
|
||||
test('close button and backdrop dismiss the drawer, not the modal', async ({ page, viewport }) => {
|
||||
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
||||
await boot(page);
|
||||
await openConfigModal(page);
|
||||
|
||||
// Close button inside the drawer header.
|
||||
await page.locator('#config-hamburger').click();
|
||||
await expect(page.locator('#config-toc-close')).toBeVisible();
|
||||
await page.locator('#config-toc-close').click();
|
||||
await expect(page.locator('#config-nav')).toBeHidden();
|
||||
await expect(page.locator('#config-modal.active')).toBeVisible();
|
||||
|
||||
// Backdrop tap closes the drawer first; the modal stays open.
|
||||
// Click near the right edge of the modal (outside the left drawer).
|
||||
await page.locator('#config-hamburger').click();
|
||||
await expect(page.locator('#config-nav')).toBeVisible();
|
||||
await page.locator('#config-modal').click({ position: { x: (viewport?.width ?? 393) - 8, y: 200 } });
|
||||
await expect(page.locator('#config-nav')).toBeHidden();
|
||||
await expect(page.locator('#config-modal.active')).toBeVisible();
|
||||
});
|
||||
|
||||
test('picking a section scrolls to it and collapses the drawer', async ({ page, viewport }) => {
|
||||
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
||||
await boot(page);
|
||||
await openConfigModal(page);
|
||||
@@ -57,7 +91,7 @@ test.describe('Configurations modal on mobile (BUG-071)', () => {
|
||||
await expect(link).toBeVisible();
|
||||
await link.click();
|
||||
|
||||
// Nav collapses on mobile after selection…
|
||||
// Drawer collapses on mobile after selection…
|
||||
await expect(page.locator('#config-nav')).toBeHidden();
|
||||
// …the link is marked active…
|
||||
await expect(link).toHaveClass(/active/);
|
||||
@@ -76,6 +110,37 @@ test.describe('Configurations modal on mobile (BUG-071)', () => {
|
||||
.toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('modal is full-screen and key controls meet the 44px touch target', async ({ page, viewport }) => {
|
||||
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
||||
await boot(page);
|
||||
await openConfigModal(page);
|
||||
|
||||
// Full-screen: the modal container fills the viewport.
|
||||
const container = page.locator('#config-modal .editor-container');
|
||||
const box = await container.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box.width).toBeGreaterThanOrEqual((viewport?.width ?? 0) - 2);
|
||||
expect(box.height).toBeGreaterThanOrEqual((viewport?.height ?? 0) - 2);
|
||||
|
||||
// Header controls (hamburger + close) are ≥44px.
|
||||
for (const sel of ['#config-hamburger', '#config-close']) {
|
||||
const b = await page.locator(sel).boundingBox();
|
||||
expect(b, `${sel} has no bounding box`).not.toBeNull();
|
||||
expect(b.height, `${sel} height`).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
||||
expect(b.width, `${sel} width`).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
||||
}
|
||||
|
||||
// Backend Save row: buttons are ≥44px tall.
|
||||
await page.locator('#config-hamburger').click();
|
||||
await page.locator('#config-nav a[href="#cfg-backend-settings"]').click();
|
||||
await expect(page.locator('#config-nav')).toBeHidden();
|
||||
const saveBtn = page.locator('#cfg-save-backend');
|
||||
await expect(saveBtn).toBeVisible();
|
||||
const saveBox = await saveBtn.boundingBox();
|
||||
expect(saveBox).not.toBeNull();
|
||||
expect(saveBox.height).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
||||
});
|
||||
|
||||
test('no horizontal overflow at 393px', async ({ page, viewport }) => {
|
||||
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
||||
await boot(page);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ObsiGate — Configurations modal mobile usability non-regression tests (BUG-071).
|
||||
* ObsiGate — Configurations modal mobile usability tests (BUG-071 + #114).
|
||||
*
|
||||
* Static checks (no jsdom needed — runs in the "Frontend unit tests" CI step):
|
||||
* - BUG-071a: #config-nav shared the .help-nav rule that hides it below
|
||||
@@ -18,8 +18,16 @@
|
||||
* style.css must stack/wrap them below 768px with 44px touch targets.
|
||||
* - BUG-071d: every #config-nav link target must exist (dead-anchor guard,
|
||||
* same class of bug as BUG-067 for the help modal).
|
||||
* - BUG-071e: data-i18n-attr supports several "attr:key" pairs (";"-
|
||||
* - BUG-071e: data-i18n-attr supports several "attr:key" pairs (";"
|
||||
* separated) so the toggle carries translated title AND aria-label.
|
||||
* - #114a: the TOC is a left slide-over drawer (position: fixed) opened by
|
||||
* .config-toc-open on #config-modal, with a backdrop (::before) and a
|
||||
* close button (#config-toc-close).
|
||||
* - #114b: the modal is full-screen (100dvh), inputs are ≥16px anti-zoom
|
||||
* with 44px min-height, buttons are 44px touch targets, the backend Save
|
||||
* row is sticky, and the MFA recovery list is single-column on mobile.
|
||||
* - #114i18n: dead settings.* keys purged, settings.explorer and
|
||||
* config.toc_close present in FR and EN, settings.tabs translated in FR.
|
||||
*
|
||||
* Usage: node tests/frontend/config-mobile.test.mjs
|
||||
*/
|
||||
@@ -82,12 +90,17 @@ test("config.js — TOC links smooth-scroll, mark active, collapse on mobile", (
|
||||
|
||||
test("config.js — TOC display reset when the modal opens", () => {
|
||||
assert.match(configJs, /configNavOnOpen[\s\S]{0,120}?style\.display = ''/, "stale inline display would stick across sessions");
|
||||
assert.match(configJs, /modal\.classList\.remove\("config-toc-open"\)/,
|
||||
"the drawer-open class (backdrop) must be cleared on open");
|
||||
});
|
||||
|
||||
// ── BUG-071c: mobile CSS ────────────────────────────────────────────────────
|
||||
test("style.css — config TOC becomes a capped top block on mobile", () => {
|
||||
test("style.css — config TOC becomes a fixed drawer on mobile", () => {
|
||||
assert.match(css, /#config-modal #config-nav/, "no mobile rule scoped to #config-modal #config-nav");
|
||||
assert.match(css, /#config-modal #config-nav[\s\S]{0,400}?max-height/, "the opened TOC must be height-capped so content stays reachable");
|
||||
assert.match(css, /#config-modal #config-nav[\s\S]{0,400}?position: fixed/,
|
||||
"the TOC must be a fixed slide-over drawer on mobile");
|
||||
assert.match(css, /#config-modal #config-nav[\s\S]{0,400}?max-height/,
|
||||
"the drawer must be height-capped so content stays reachable");
|
||||
});
|
||||
|
||||
test("style.css — two-column config grids stack on mobile", () => {
|
||||
@@ -124,6 +137,136 @@ test("i18n.js — data-i18n-attr supports several attr:key pairs", () => {
|
||||
assert.match(i18nJs, /el\.setAttribute\(attr, t\(key\)\)/, "each pair must set its attribute");
|
||||
});
|
||||
|
||||
// ── #114a: drawer, backdrop, close button ───────────────────────────────────
|
||||
test("index.html — #config-toc-close exists inside the TOC header", () => {
|
||||
const nav = indexHtml.match(/<nav class="help-nav" id="config-nav">([\s\S]*?)<\/nav>/);
|
||||
assert.ok(nav, "#config-nav not found");
|
||||
assert.match(nav[1], /id="config-toc-close"/, "no #config-toc-close in the TOC — no way to dismiss the drawer");
|
||||
assert.match(nav[1], /config\.toc_close/, "the close button label must use the i18n key config.toc_close");
|
||||
});
|
||||
|
||||
test("i18n — config.toc_close exists in FR and EN", () => {
|
||||
assert.ok(fr["config.toc_close"], "fr.json missing config.toc_close");
|
||||
assert.ok(en["config.toc_close"], "en.json missing config.toc_close");
|
||||
assert.notEqual(fr["config.toc_close"], "config.toc_close", "FR value must be translated");
|
||||
assert.notEqual(en["config.toc_close"], "config.toc_close", "EN value must be translated");
|
||||
});
|
||||
|
||||
test("config.js — drawer open class + close button + backdrop + Escape", () => {
|
||||
assert.match(configJs, /classList\.toggle\("config-toc-open"/,
|
||||
"_setConfigNav must toggle .config-toc-open on the modal (backdrop + drawer CSS)");
|
||||
assert.match(configJs, /getElementById\("config-toc-close"\)/,
|
||||
"no binding on #config-toc-close");
|
||||
assert.match(configJs, /config-toc-open[\s\S]{0,200}?_setConfigNav\(false\)/,
|
||||
"a backdrop tap must close the drawer first, not the whole modal");
|
||||
assert.match(configJs, /Escape[\s\S]{0,200}?config-toc-open[\s\S]{0,120}?_setConfigNav\(false\)/,
|
||||
"Escape must close the drawer first, then the modal");
|
||||
assert.match(configJs, /closeConfigModal[\s\S]{0,200}?classList\.remove\("config-toc-open"\)/,
|
||||
"closeConfigModal must never leave the backdrop class behind");
|
||||
});
|
||||
|
||||
test("style.css — drawer backdrop via .config-toc-open::before", () => {
|
||||
assert.match(css, /#config-modal\.config-toc-open::before/,
|
||||
"no backdrop pseudo-element for the open drawer");
|
||||
assert.match(css, /#config-modal\.config-toc-open::before[\s\S]{0,200}?z-index: 35/,
|
||||
"the backdrop must sit above the content and below the drawer (z-index 40)");
|
||||
assert.match(css, /#config-modal\.config-toc-open #config-nav/,
|
||||
"the open drawer must be forced visible via the modal class");
|
||||
});
|
||||
|
||||
test("style.css — .help-toc-close hidden on desktop, visible in the config drawer", () => {
|
||||
assert.match(css, /\.help-toc-close\s*\{[^}]*display:\s*none/,
|
||||
".help-toc-close must be hidden by default (desktop nav is always visible)");
|
||||
assert.match(css, /#config-modal \.help-toc-close\s*\{[^}]*display:\s*inline-flex/,
|
||||
"#config-modal .help-toc-close must show on mobile");
|
||||
});
|
||||
|
||||
// ── #114b: full-screen, anti-zoom, touch targets, sticky save ───────────────
|
||||
test("style.css — config modal goes full-screen with 100dvh on mobile", () => {
|
||||
assert.match(css, /#config-modal \.editor-container[\s\S]{0,300}?100dvh/,
|
||||
"the modal must fill the dynamic viewport (mobile browser chrome)");
|
||||
assert.match(css, /#config-modal[\s\S]{0,200}?padding:\s*0/,
|
||||
"the modal must drop its outer padding on mobile");
|
||||
});
|
||||
|
||||
test("style.css — inputs are ≥16px with 44px min-height (anti-zoom iOS)", () => {
|
||||
assert.match(css, /#config-modal \.config-input[\s\S]{0,200}?font-size:\s*16px/,
|
||||
"config inputs must be ≥16px to stop iOS auto-zoom");
|
||||
assert.match(css, /#config-modal \.config-input[\s\S]{0,200}?min-height:\s*44px/,
|
||||
"config inputs need a 44px touch target");
|
||||
assert.match(css, /#config-modal \.config-select[\s\S]{0,200}?font-size:\s*16px/,
|
||||
"config selects must be ≥16px to stop iOS auto-zoom");
|
||||
});
|
||||
|
||||
test("style.css — primary buttons are 44px touch targets", () => {
|
||||
// The shared button block lists many selectors; match from the block start
|
||||
// (first #config-modal .config-btn-save) to the min-height declaration.
|
||||
const buttonBlock = css.match(/#config-modal \.config-btn-save,[\s\S]{0,600}?\{[^}]*min-height:\s*44px/);
|
||||
assert.ok(buttonBlock, ".config-btn-save needs a 44px touch target");
|
||||
for (const cls of ["config-btn-secondary", "config-btn-danger", "config-btn-sm"]) {
|
||||
assert.match(css, new RegExp(`#config-modal \\.${cls}`), `#${cls} has no #114 mobile rule`);
|
||||
}
|
||||
assert.match(css, /#config-modal \.config-actions-row[\s\S]{0,400}?min-height:\s*44px/,
|
||||
"buttons inside the sticky Save row need a 44px touch target");
|
||||
});
|
||||
|
||||
test("style.css — backend Save row is sticky at the bottom of its section", () => {
|
||||
assert.match(css, /#config-modal \.config-actions-row\s*\{[^}]*position:\s*sticky/,
|
||||
"the Save row must stick to the bottom while scrolling the backend section");
|
||||
assert.match(css, /#config-modal \.config-actions-row[\s\S]{0,300}?flex-direction:\s*column/,
|
||||
"the Save row must stack vertically on mobile");
|
||||
assert.match(css, /#config-modal \.config-actions-row[\s\S]{0,300}?env\(safe-area-inset-bottom/,
|
||||
"the sticky Save row must clear the home indicator");
|
||||
});
|
||||
|
||||
test("style.css — MFA recovery list single-column + verify section wraps", () => {
|
||||
assert.match(css, /#config-modal \.mfa-recovery-list\s*\{[^}]*grid-template-columns:\s*1fr/,
|
||||
"recovery codes must be a single column on mobile");
|
||||
assert.match(css, /#config-modal \.mfa-verify-section[\s\S]{0,200}?flex-wrap:\s*wrap/,
|
||||
"the MFA verify row must wrap instead of overflowing");
|
||||
assert.match(css, /#config-modal \.mfa-code-input[\s\S]{0,300}?width:\s*100%/,
|
||||
"the MFA code input must be full-width on mobile");
|
||||
});
|
||||
|
||||
test("index.html — .config-actions-row lives inside #cfg-backend-settings", () => {
|
||||
const section = indexHtml.match(/<section[^>]*id="cfg-backend-settings"[\s\S]*?<\/section>/);
|
||||
assert.ok(section, "#cfg-backend-settings section not found");
|
||||
assert.match(section[0], /class="config-actions-row"/,
|
||||
".config-actions-row must be inside the backend section (sticky footer contract)");
|
||||
});
|
||||
|
||||
// ── #114i18n: dead keys purged, new keys added ──────────────────────────────
|
||||
test("i18n — dead settings.* keys are purged from FR and EN", () => {
|
||||
const dead = ["settings.backend", "settings.backend_hint", "settings.restart_badge",
|
||||
"settings.save", "settings.plugins"];
|
||||
for (const key of dead) {
|
||||
assert.equal(fr[key], undefined, `fr.json still has dead key ${key}`);
|
||||
assert.equal(en[key], undefined, `en.json still has dead key ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("i18n — settings.explorer exists in FR and EN", () => {
|
||||
assert.ok(fr["settings.explorer"], "fr.json missing settings.explorer");
|
||||
assert.ok(en["settings.explorer"], "en.json missing settings.explorer");
|
||||
assert.notEqual(fr["settings.explorer"], "settings.explorer", "FR value must be translated");
|
||||
assert.notEqual(en["settings.explorer"], "settings.explorer", "EN value must be translated");
|
||||
});
|
||||
|
||||
test("i18n — settings.tabs is translated in FR (was the English word \"Tabs\")", () => {
|
||||
assert.equal(fr["settings.tabs"], "Onglets", "fr.json settings.tabs must be French");
|
||||
assert.equal(en["settings.tabs"], "Tabs", "en.json settings.tabs must stay English");
|
||||
});
|
||||
|
||||
test("index.html — #mt-explorer label carries data-i18n=settings.explorer", () => {
|
||||
assert.match(indexHtml, /id="mt-explorer"[\s\S]{0,400}?data-i18n="settings\.explorer"/,
|
||||
"the explorer toolbar button must be translated via settings.explorer");
|
||||
});
|
||||
|
||||
test("index.html — no dead container #plugins-settings-container", () => {
|
||||
assert.ok(!indexHtml.includes("plugins-settings-container"),
|
||||
"#plugins-settings-container was a dead div — plugins render into #cfg-plugins");
|
||||
});
|
||||
|
||||
if (process.exitCode) {
|
||||
console.error("\nConfig mobile tests FAILED");
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user