diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index 346c177..1565ad7 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -38,6 +38,7 @@ jobs:
- name: Frontend unit tests
run: |
node tests/frontend/unit.test.mjs
+ node tests/frontend/image-viewer.test.mjs
node tests/frontend/pdf-viewer.test.mjs
node tests/frontend/forge-completion.test.mjs
node tests/frontend/config-mobile.test.mjs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c74e75..2ea8d8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.20.0**.
+> [Unreleased](#unreleased). La dernière version livrée est **2.21.0**.
---
@@ -14,6 +14,28 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
+## [2.21.0] — 2026-09-23
+
+### Ajouté
+
+- **#112 — section compte en bas de la sidebar.** L'identité (avatar avec
+ initiales, nom, rôle) et la **déconnexion** sont regroupées dans un bloc
+ épinglé au bas de la barre latérale, comme sur les sites professionnels ; un
+ clic sur l'identité ouvre le Profil. Nouvelle clef i18n FR/EN pour le rôle.
+- **#112 — pellicule d'images défilable.** La molette de la souris fait défiler
+ horizontalement la bande de miniatures et deux **flèches translucides** sur les
+ côtés (révélées au survol) remplissent la même fonction.
+
+### Modifié
+
+- **#112 — en-tête allégé.** La **version** quitte le header pour le menu
+ **Options** (ligne « Version ») ; le **bouton de déconnexion** et le **nom de
+ l'utilisateur** sont retirés du header au profit de la section compte de la
+ sidebar. Le header droit ne conserve que l'indicateur hors-ligne, le contexte
+ vault et le bouton Options.
+
+---
+
## [2.20.0] — 2026-09-23
### Ajouté
diff --git a/README.fr.md b/README.fr.md
index 9ccff85..68854c7 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -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.20.0).
+Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.21.0).
---
-*Projet : ObsiGate | Version : 2.20.0 | Dernière mise à jour : Septembre 2026*
+*Projet : ObsiGate | Version : 2.21.0 | Dernière mise à jour : Septembre 2026*
diff --git a/README.md b/README.md
index f77a701..968a565 100644
--- a/README.md
+++ b/README.md
@@ -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.20.0).
+See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.21.0).
---
-*Project: ObsiGate | Version: 2.20.0 | Last updated: September 2026*
+*Project: ObsiGate | Version: 2.21.0 | Last updated: September 2026*
diff --git a/VERSION b/VERSION
index 7329e21..db65e21 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.20.0
+2.21.0
diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock
index 4d818fd..d5fabbb 100644
--- a/desktop/Cargo.lock
+++ b/desktop/Cargo.lock
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
-version = "2.20.0"
+version = "2.21.0"
dependencies = [
"chrono",
"env_logger",
diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml
index bd01a95..56c371c 100644
--- a/desktop/Cargo.toml
+++ b/desktop/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
-version = "2.20.0"
+version = "2.21.0"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json
index 671de7a..3d192c8 100644
--- a/desktop/tauri.conf.json
+++ b/desktop/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
- "version": "2.20.0",
+ "version": "2.21.0",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index eee8c72..9a040c6 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
-> **Version :** 2.20.0 | **Dernière mise à jour :** 2026-09-23
+> **Version :** 2.21.0 | **Dernière mise à jour :** 2026-09-23
> **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)**
@@ -184,6 +184,7 @@
| 109 | Support audio & vidéo — lecteurs HTML5 intégrés, streaming HTTP Range (`/api/media`), fallback codec/taille | 2.18.0 | [features/media-viewers-109.md](./features/media-viewers-109.md) |
| 110 | Lecteur média persistant « Now Playing » — élément partagé téléporté (inline ⇄ dock), Media Session, mini-vidéo PiP, mobile, reprise | 2.19.0 | [features/media-viewers-109.md](./features/media-viewers-109.md) |
| 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) |
---
@@ -191,7 +192,7 @@
| Priorité | Items | Effort total estimé |
|---|---|---|
-| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–111, #92 | ~130 jours réalisés |
+| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–112, #92 | ~130 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 |
diff --git a/docs/features/header-user-sidebar-112.md b/docs/features/header-user-sidebar-112.md
new file mode 100644
index 0000000..0713a41
--- /dev/null
+++ b/docs/features/header-user-sidebar-112.md
@@ -0,0 +1,69 @@
+# #112 — En-tête allégé & compte en sidebar
+
+> **Version livrée :** 2.21.0 · **Statut :** 🟢 · **Impact :** 🟡
+> **Zone :** frontend (`index.html`, `frontend/js/auth.js`, `frontend/js/viewer.js`,
+> `frontend/style.css`, i18n FR/EN) + CI (`.gitea/workflows/ci.yml`).
+
+## Contexte
+
+Le header concentrait plusieurs éléments redondants ou peu lisibles :
+
+- la **version** était affichée en permanence à côté du bouton Options ;
+- un **bouton de déconnexion** et le **nom de l'utilisateur** occupaient la zone
+ droite du header, sans regroupement logique ;
+- la pellicule de miniatures de la visionneuse d'images ne pouvait être
+ parcourue qu'au glisser (pas de molette horizontale ni de flèches dédiées).
+
+## Ce qui a été livré
+
+### A. Header allégé
+
+- **Version déplacée** du header vers le menu **Options** : nouvelle ligne
+ non-interactive « Version » (`#version-badge` conservé pour `populateVersions`).
+- **Suppression** du bloc `#user-menu` (nom + bouton « sign out ») et de la
+ ligne « Déconnexion » du menu Options. Le header droit ne garde que l'indicateur
+ hors-ligne, le contexte vault et le bouton Options.
+
+### B. Section compte en bas de la sidebar
+
+- Nouveau bloc `#sidebar-user` épinglé au bas de la sidebar (`flex-shrink: 0`,
+ bordure supérieure), à la manière d'un site web professionnel :
+ - initiales dans un **avatar** circulaire (dégradé d'accent) ;
+ - **nom** (`#sidebar-user-name`, classe `.user-display-name` conservée pour la
+ réutilisation par la collaboration) et **rôle** localisé
+ (Administrateur / Utilisateur) ;
+ - **bouton déconnexion** (`#sidebar-user-logout`) ;
+ - clic sur l'identité → **Profil** (ouvre la section `#cfg-profile`).
+- `AuthManager.renderUserSection()` peuple le bloc ; il reste masqué quand
+ l'authentification est désactivée (`#sidebar-user[hidden]`). `renderUserMenu()`
+ est conservé comme alias rétro-compatible.
+- La règle mobile qui masquait `.user-display-name` est limitée au header
+ (`.header-right .user-display-name`) pour que le nom reste visible dans la
+ sidebar mobile.
+
+### C. Pellicule d'images défilable (#112, complément de #111)
+
+- Nouveau conteneur `.image-nav` autour de la pellicule, avec deux **flèches
+ translucides** fixes (`.image-strip-arrow-prev/next`) révélées au survol.
+- **Molette** au-dessus de la pellicule → défilement horizontal
+ (`strip.scrollLeft += deltaY`), conversion des deltas verticaux.
+- Les flèches font défiler d'une « page » (`strip.scrollBy`, défilement doux).
+
+## Tests
+
+- `tests/frontend/unit.test.mjs` (+1) : header nettoyé (plus de `#user-menu`,
+ `#logout-btn`, ni version dans `.header-right`), version dans le menu Options,
+ présence de `#sidebar-user`, styles `.sidebar-user`/`.menu-list-version`, clés
+ i18n FR/EN.
+- `tests/frontend/image-viewer.test.mjs` (+1) : wrapper `.image-nav`, flèches de
+ pellicule, molette et `scrollBy`.
+- `tests/e2e/header-sidebar.spec.js` (nouveau) : test header/version exécuté
+ partout ; test compte en sidebar conditionné à l'auth (skip sinon).
+- CI : `image-viewer.test.mjs` ajouté au job `lint` (il n'y était pas depuis #108).
+
+## Limitations connues
+
+- La section compte n'apparaît que si l'authentification est activée.
+- `doLogoutFallback` (script inline supprimé avec la ligne du menu) reste appelé
+ par la section Profil uniquement en repli ; `window.handleLogout` est toujours
+ défini par `auth.js`, donc le repli n'est jamais atteint.
diff --git a/frontend/index.html b/frontend/index.html
index 37a29fd..4834917 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -519,9 +519,6 @@
>
All
-
-
-
-
-
+
@@ -1052,6 +1049,43 @@
Aucune conversation
+
+
+
+
+
+
@@ -3293,6 +3327,10 @@
Tags: Nuage de tags
cliquables
+
+ Compte: Avatar, nom,
+ rôle et déconnexion en bas de la sidebar
+
Fonctionnalités de la sidebar :
diff --git a/frontend/js/auth.js b/frontend/js/auth.js
index cd1c208..e9269c2 100644
--- a/frontend/js/auth.js
+++ b/frontend/js/auth.js
@@ -1,7 +1,7 @@
/* ObsiGate — Authentication: API helper, AuthManager, login form, AdminPanel */
import { state } from './state.js';
import { safeCreateIcons, escapeHtml } from './utils.js';
-import { showToast, closeHeaderMenu } from './ui.js';
+import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
import { t, getLocale, setLocale } from './i18n.js';
import { showWelcome } from './viewer.js';
@@ -13,6 +13,15 @@ window.handleLogout = () => {
AuthManager.logout();
};
+/** Two-letter initials for the sidebar account avatar (#112). */
+function userInitials(name) {
+ const parts = String(name || "").trim().split(/\s+/).filter(Boolean);
+ if (!parts.length) return "?";
+ const first = parts[0][0] || "";
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
+ return (first + last).toUpperCase() || "?";
+}
+
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
@@ -323,22 +332,46 @@ const AuthManager = {
const app = document.getElementById("app");
if (login) login.classList.add("hidden");
if (app) app.classList.remove("hidden");
- this.renderUserMenu();
+ this.renderUserSection();
},
- renderUserMenu() {
+ /**
+ * Render the account block pinned at the bottom of the sidebar (#112) —
+ * the user identity/logout no longer live in the header.
+ */
+ renderUserSection() {
const user = this.getUser();
- const userMenu = document.getElementById("user-menu");
- if (!userMenu) return;
+ const section = document.getElementById("sidebar-user");
if (!user || !this._authEnabled) {
- userMenu.innerHTML = "";
+ if (section) section.hidden = true;
return;
}
- userMenu.innerHTML = '' + (user.display_name || user.username) + "" + '';
+ if (!section) return;
+
+ const name = user.display_name || user.username || "";
+ const nameEl = document.getElementById("sidebar-user-name");
+ if (nameEl) nameEl.textContent = name;
+ const roleEl = document.getElementById("sidebar-user-role");
+ if (roleEl) roleEl.textContent = user.role === "admin" ? t("sidebar.user_role_admin") : t("sidebar.user_role_user");
+ const avatarEl = document.getElementById("sidebar-user-avatar");
+ if (avatarEl) avatarEl.textContent = userInitials(name);
+ section.hidden = false;
safeCreateIcons();
- const logoutBtn = document.getElementById("logout-btn");
- if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout());
+ const logoutBtn = document.getElementById("sidebar-user-logout");
+ if (logoutBtn && !logoutBtn._obsigateBound) {
+ logoutBtn._obsigateBound = true;
+ logoutBtn.addEventListener("click", () => this.logout());
+ }
+ const profileBtn = document.getElementById("sidebar-user-profile");
+ if (profileBtn && !profileBtn._obsigateBound) {
+ profileBtn._obsigateBound = true;
+ profileBtn.addEventListener("click", () => {
+ closeMobileSidebar();
+ const trigger = document.getElementById("profile-open-btn");
+ if (trigger) trigger.click();
+ });
+ }
const adminRow = document.getElementById("admin-menu-row");
if (adminRow) {
@@ -355,6 +388,9 @@ const AuthManager = {
}
},
+ // Backwards-compatible alias (older callers/tests may use this name).
+ renderUserMenu() { this.renderUserSection(); },
+
// ── Initialization ──────────────────────────────────────────────
async checkAuthStatus() {
diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js
index efb3d67..efa4505 100644
--- a/frontend/js/viewer.js
+++ b/frontend/js/viewer.js
@@ -700,8 +700,26 @@ export function renderImageViewer(area, data) {
container.appendChild(body);
// ── Thumbnail filmstrip (navigation) ───────────────────────────────────
- const strip = el("div", { class: "image-nav-strip", hidden: true });
- container.appendChild(strip);
+ // #112 — the film scrolls with the mouse wheel and two translucent edge
+ // arrows; the scrollable strip is wrapped so the arrows stay fixed.
+ const navWrap = el("div", { class: "image-nav", hidden: true });
+ const strip = el("div", { class: "image-nav-strip" });
+ const stripPrev = el("button", {
+ class: "image-strip-arrow image-strip-arrow-prev",
+ type: "button",
+ title: t("viewer.image_strip_prev"),
+ "aria-label": t("viewer.image_strip_prev"),
+ }, [icon("chevron-left", 20)]);
+ const stripNext = el("button", {
+ class: "image-strip-arrow image-strip-arrow-next",
+ type: "button",
+ title: t("viewer.image_strip_next"),
+ "aria-label": t("viewer.image_strip_next"),
+ }, [icon("chevron-right", 20)]);
+ navWrap.appendChild(strip);
+ navWrap.appendChild(stripPrev);
+ navWrap.appendChild(stripNext);
+ container.appendChild(navWrap);
let scale = 1;
let tx = 0;
@@ -807,8 +825,8 @@ export function renderImageViewer(area, data) {
};
const renderStrip = () => {
- if (siblings.length < 2) { strip.hidden = true; return; }
- strip.hidden = false;
+ if (siblings.length < 2) { navWrap.hidden = true; return; }
+ navWrap.hidden = false;
strip.innerHTML = "";
thumbs = siblings.map((s, i) => {
const thumb = el("img", {
@@ -826,6 +844,20 @@ export function renderImageViewer(area, data) {
updateStripActive();
};
+ // #112 — scroll the film with the wheel (vertical → horizontal) and the
+ // two overlay arrows; the active image never changes, only the strip view.
+ const scrollStrip = (dir) => {
+ strip.scrollBy({ left: dir * Math.max(140, strip.clientWidth * 0.8), behavior: "smooth" });
+ };
+ stripPrev.addEventListener("click", () => scrollStrip(-1));
+ stripNext.addEventListener("click", () => scrollStrip(1));
+ strip.addEventListener("wheel", (e) => {
+ if (strip.scrollWidth <= strip.clientWidth) return;
+ const delta = Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
+ strip.scrollLeft += delta;
+ e.preventDefault();
+ }, { passive: false });
+
(async () => {
try {
const dir = currentPath.includes("/") ? currentPath.slice(0, currentPath.lastIndexOf("/")) : "";
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index 66e8264..883c8db 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -777,6 +777,7 @@
"header.menu_theme": "Theme",
"header.menu_theme_light": "Light",
"header.menu_toggle": "Menu",
+ "header.menu_version": "Version",
"header.profile": "Profile",
"header.quick_help": "Quick Help",
"header.refresh": "Refresh",
@@ -1106,7 +1107,7 @@
"help.desc_markdown_all": ": Full markdown rendering",
"help.desc_md_render": "Markdown files are rendered with:",
"help.desc_menu_header": ": \"Vault\" dropdown in the Options menu",
- "help.desc_menu_options": ": Access to settings, theme, help",
+ "help.desc_menu_options": ": Access to settings, theme, help and version",
"help.desc_op_ext": ": Filter by file type",
"help.desc_op_path": ": Filter by path",
"help.desc_op_phrase": ": Exact phrase search",
@@ -1309,6 +1310,8 @@
"help.sidebar_filtering": "Sidebar filtering",
"help.sidebar_section": "Sidebar",
"help.sidebar_tabs": "The sidebar has two tabs:",
+ "help.sidebar_user": "Account",
+ "help.desc_sidebar_user": ": Avatar, name, role and sign-out at the bottom of the sidebar",
"help.sidebar_toggle_btn": "Sidebar toggle button",
"help.simple": "Simple",
"help.simple_search": "Simple search",
@@ -1682,6 +1685,8 @@
"sidebar.tab_saved": "Searches",
"sidebar.tab_tags": "Tags",
"sidebar.tab_vaults": "Vaults",
+ "sidebar.user_role_admin": "Administrator",
+ "sidebar.user_role_user": "User",
"sidebar.vault_context_all": "All vaults",
"sidebar.vault_specific": "Specific vault",
"sync.autocomplete": "Autocomplete",
@@ -1830,6 +1835,8 @@
"viewer.image_next": "Next image",
"viewer.image_open_original": "Open original",
"viewer.image_prev": "Previous image",
+ "viewer.image_strip_next": "Scroll thumbnails right",
+ "viewer.image_strip_prev": "Scroll thumbnails left",
"viewer.image_type": "Type",
"viewer.image_zoom_in": "Zoom in",
"viewer.image_zoom_out": "Zoom out",
diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json
index 0850017..103e99c 100644
--- a/frontend/locales/fr.json
+++ b/frontend/locales/fr.json
@@ -777,6 +777,7 @@
"header.menu_theme": "Thème",
"header.menu_theme_light": "Clair",
"header.menu_toggle": "Menu",
+ "header.menu_version": "Version",
"header.profile": "Profil",
"header.quick_help": "Raccourcis & Astuces",
"header.refresh": "Rafraîchir",
@@ -1106,7 +1107,7 @@
"help.desc_markdown_all": ":\n Rendu complet du markdown",
"help.desc_md_render": "Les fichiers markdown sont rendus avec :",
"help.desc_menu_header": ": Dropdown \"Vault\" dans le menu Options",
- "help.desc_menu_options": ": Accès aux\n paramètres, thème, aide",
+ "help.desc_menu_options": ": Accès aux\n paramètres, thème, aide et version",
"help.desc_op_ext": ": Filtrer par type de fichier",
"help.desc_op_path": ": Filtrer par chemin",
"help.desc_op_phrase": ": Recherche de phrase entre guillemets",
@@ -1309,6 +1310,8 @@
"help.sidebar_filtering": "Filtrage de la sidebar",
"help.sidebar_section": "Sidebar (barre latérale)",
"help.sidebar_tabs": "La sidebar est divisée en deux onglets :",
+ "help.sidebar_user": "Compte",
+ "help.desc_sidebar_user": ": Avatar, nom, rôle et déconnexion en bas de la sidebar",
"help.sidebar_toggle_btn": "Bouton toggle sidebar",
"help.simple": "Simple",
"help.simple_search": "Recherche simple",
@@ -1682,6 +1685,8 @@
"sidebar.tab_saved": "Recherches",
"sidebar.tab_tags": "Tags",
"sidebar.tab_vaults": "Vaults",
+ "sidebar.user_role_admin": "Administrateur",
+ "sidebar.user_role_user": "Utilisateur",
"sidebar.vault_context_all": "Toutes les vaults",
"sidebar.vault_specific": "Vault spécifique",
"sync.autocomplete": "Auto-complétion",
@@ -1830,6 +1835,8 @@
"viewer.image_next": "Image suivante",
"viewer.image_open_original": "Ouvrir l'original",
"viewer.image_prev": "Image précédente",
+ "viewer.image_strip_next": "Faire défiler les miniatures vers la droite",
+ "viewer.image_strip_prev": "Faire défiler les miniatures vers la gauche",
"viewer.image_type": "Type",
"viewer.image_zoom_in": "Zoom avant",
"viewer.image_zoom_out": "Zoom arrière",
diff --git a/frontend/style.css b/frontend/style.css
index c90d4c5..a28fd03 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -3815,8 +3815,8 @@ body.resizing-v {
gap: 8px;
}
- .user-display-name {
- display: none; /* Masquer le nom d'utilisateur sur mobile pour gagner de la place */
+ .header-right .user-display-name {
+ display: none; /* Masquer le nom d'utilisateur dans le header sur mobile */
}
.search-wrapper {
@@ -6039,6 +6039,100 @@ body.resizing-v {
color: var(--danger);
}
+/* ---------------------------------------------------------------------------
+ Sidebar account section (#112) — identity + logout pinned at the bottom
+ --------------------------------------------------------------------------- */
+.sidebar-user {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ border-top: 1px solid var(--border);
+ background: var(--bg-sidebar);
+ flex-shrink: 0;
+}
+.sidebar-user[hidden] {
+ display: none;
+}
+.sidebar-user-profile {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 8px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+ transition: background 150ms ease, border-color 150ms ease;
+}
+.sidebar-user-profile:hover {
+ background: var(--bg-hover);
+ border-color: var(--border);
+}
+.sidebar-user-avatar {
+ width: 34px;
+ height: 34px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ color: #fff;
+ background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #7c3aed));
+}
+.sidebar-user-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 0;
+}
+.sidebar-user-name {
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ max-width: none;
+}
+.sidebar-user-role {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+.sidebar-user-logout {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ flex-shrink: 0;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: color 150ms ease, background 150ms ease, border-color 150ms ease;
+}
+.sidebar-user-logout:hover {
+ color: var(--danger);
+ background: var(--bg-hover);
+ border-color: var(--border);
+}
+
+/* Version row inside the header options menu (#112) */
+.menu-list-version {
+ cursor: default;
+ border-top: 1px solid var(--border);
+ margin-top: 4px;
+ padding-top: 8px;
+}
+.menu-list-version .version-badge {
+ margin-left: 4px;
+}
+
/* ---------------------------------------------------------------------------
Admin Panel
--------------------------------------------------------------------------- */
@@ -9785,18 +9879,66 @@ body.desktop-mode .editor-container {
opacity: 0.55;
}
}
+/* #112 — filmstrip wrapper so the scroll arrows stay fixed over the film */
+.image-nav {
+ position: relative;
+ flex-shrink: 0;
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+}
+.image-nav[hidden] {
+ display: none;
+}
.image-nav-strip {
display: flex;
gap: 6px;
- padding: 8px;
+ padding: 8px 40px;
overflow-x: auto;
- background: var(--surface);
- border-top: 1px solid var(--border);
+ scroll-behavior: smooth;
+ scrollbar-width: thin;
flex-shrink: 0;
}
.image-nav-strip[hidden] {
display: none;
}
+/* Translucent arrows that scroll the film left/right (revealed on hover). */
+.image-strip-arrow {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: none;
+ color: var(--text-primary);
+ opacity: 0;
+ cursor: pointer;
+ z-index: 2;
+ transition: opacity 160ms ease;
+}
+.image-strip-arrow-prev {
+ left: 0;
+ background: linear-gradient(to right, var(--surface) 55%, transparent);
+}
+.image-strip-arrow-next {
+ right: 0;
+ background: linear-gradient(to left, var(--surface) 55%, transparent);
+}
+.image-nav:hover .image-strip-arrow {
+ opacity: 0.6;
+}
+.image-strip-arrow:hover,
+.image-strip-arrow:focus-visible {
+ opacity: 1;
+ outline: none;
+}
+@media (hover: none) {
+ .image-strip-arrow {
+ opacity: 0.55;
+ }
+}
.image-thumb {
width: 72px;
height: 72px;
@@ -9848,6 +9990,7 @@ body.desktop-mode .editor-container {
z-index: 2000;
background: rgba(0, 0, 0, 0.9);
}
+.image-viewer-container.lightbox .image-nav,
.image-viewer-container.lightbox .image-nav-strip {
display: none;
}
diff --git a/package.json b/package.json
index 93c4da0..31e7411 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "obsigate",
- "version": "2.20.0",
+ "version": "2.21.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": {
diff --git a/tests/e2e/header-sidebar.spec.js b/tests/e2e/header-sidebar.spec.js
new file mode 100644
index 0000000..9b3bea9
--- /dev/null
+++ b/tests/e2e/header-sidebar.spec.js
@@ -0,0 +1,67 @@
+/**
+ * E2E — Header allégé + section compte en bas de la sidebar (#112).
+ *
+ * Le premier test s'exécute partout ; le second (section compte) nécessite
+ * l'authentification activée et se marque `skip` sinon (job CI e2e, serveur
+ * local `run-e2e-local` en auth désactivée). Pour le valider manuellement,
+ * lancer Playwright contre l'instance Docker de test (auth active) :
+ *
+ * BASE_URL=http://localhost:2020 OBSIGATE_PASS=test123 \
+ * npx playwright test tests/e2e/header-sidebar.spec.js --project=chromium-desktop
+ */
+
+import { test, expect } from '@playwright/test';
+
+const BASE = process.env.BASE_URL || 'http://localhost:2029';
+
+const CREDS = {
+ username: process.env.OBSIGATE_USER || 'admin',
+ password: process.env.OBSIGATE_PASS || 'test123',
+};
+
+async function login(page) {
+ await page.goto(BASE);
+ const loginForm = page.locator('#login-screen');
+ await expect(loginForm).toBeVisible({ timeout: 5000 }).catch(() => {});
+ if (await loginForm.isVisible()) {
+ await page.fill('#login-username', CREDS.username);
+ await page.fill('#login-password', CREDS.password);
+ await page.click('#login-btn');
+ }
+ await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
+}
+
+async function authEnabled(page) {
+ return page.evaluate(async () => {
+ try {
+ const r = await fetch('/api/auth/status');
+ const d = await r.json();
+ return !!d.auth_enabled;
+ } catch {
+ return false;
+ }
+ });
+}
+
+test.describe('Header allégé + compte en sidebar (#112)', () => {
+ test('le header ne porte plus utilisateur/version/déconnexion ; la version est dans le menu Options', async ({ page }) => {
+ await login(page);
+
+ await expect(page.locator('.header-right #user-menu')).toHaveCount(0);
+ await expect(page.locator('#logout-btn')).toHaveCount(0);
+ await expect(page.locator('.header-right > .version-badge')).toHaveCount(0);
+
+ await page.locator('#header-menu-btn').click();
+ await expect(page.locator('#header-menu-dropdown #version-badge')).toBeVisible();
+ });
+
+ test('le compte est affiché en bas de la sidebar (auth active)', async ({ page }) => {
+ await login(page);
+ test.skip(!(await authEnabled(page)), 'auth disabled in this environment');
+
+ await expect(page.locator('#sidebar-user')).toBeVisible();
+ await expect(page.locator('#sidebar-user-name')).not.toBeEmpty();
+ await expect(page.locator('#sidebar-user-role')).not.toBeEmpty();
+ await expect(page.locator('#sidebar-user-logout')).toBeVisible();
+ });
+});
diff --git a/tests/e2e/image-viewer.spec.js b/tests/e2e/image-viewer.spec.js
index 37b2e7f..85911e7 100644
--- a/tests/e2e/image-viewer.spec.js
+++ b/tests/e2e/image-viewer.spec.js
@@ -159,4 +159,31 @@ test.describe('Image viewer — zoom / pan / navigation (#108)', () => {
await expect(container).toHaveAttribute('data-inplace', '1');
await expect(page.locator('#content-area .image-counter')).toContainText('/');
});
+
+ test('la pellicule est défilable (molette + flèches latérales) sans changer d\'image (#112)', async ({ page }) => {
+ await login(page);
+ await openFile(page, 'TestVault', 'sample-image.png');
+
+ const nav = page.locator('#content-area .image-nav');
+ const strip = page.locator('#content-area .image-nav-strip');
+ await expect(strip).toBeVisible({ timeout: 10000 });
+ await expect(nav).toBeAttached();
+
+ // Les deux flèches de défilement existent (translucides, révélées au survol).
+ const prev = page.locator('#content-area .image-strip-arrow-prev');
+ const next = page.locator('#content-area .image-strip-arrow-next');
+ await expect(prev).toBeAttached();
+ await expect(next).toBeAttached();
+ await expect
+ .poll(() => next.evaluate((el) => getComputedStyle(el).opacity))
+ .not.toBe('1'); // au repos, la flèche n'est pas pleinement visible
+
+ // Faire défiler la pellicule ne change PAS l'image courante.
+ const title = page.locator('#content-area .image-title');
+ const before = await title.innerText();
+ await next.hover();
+ await next.click();
+ await prev.click();
+ await expect(title).toHaveText(before);
+ });
});
diff --git a/tests/frontend/image-viewer.test.mjs b/tests/frontend/image-viewer.test.mjs
index 1cce8a1..403f7a2 100644
--- a/tests/frontend/image-viewer.test.mjs
+++ b/tests/frontend/image-viewer.test.mjs
@@ -122,6 +122,21 @@ test("image viewer navigates between siblings in place (#111)", () => {
assert.match(viewer, /const _imageDirCache = new Map\(\)/);
});
+// #112 — the filmstrip scrolls horizontally (mouse wheel + two edge arrows).
+test("image viewer filmstrip scrolls with the wheel and edge arrows (#112)", () => {
+ assert.match(viewer, /const navWrap = el\("div", \{ class: "image-nav", hidden: true \}\)/);
+ assert.match(viewer, /image-strip-arrow-prev/);
+ assert.match(viewer, /image-strip-arrow-next/);
+ assert.match(viewer, /strip\.addEventListener\("wheel"/);
+ assert.match(viewer, /strip\.scrollBy\(\{ left: dir/);
+ assert.match(viewer, /navWrap\.hidden = true/);
+ assert.match(viewer, /navWrap\.hidden = false/);
+ assert.match(css, /\.image-nav\s*\{[\s\S]*?position:\s*relative/);
+ assert.match(css, /\.image-strip-arrow\s*\{/);
+ assert.match(css, /\.image-strip-arrow-prev\s*\{\s*left:\s*0/);
+ assert.match(css, /\.image-strip-arrow-next\s*\{\s*right:\s*0/);
+});
+
// ── Static checks: CSS ─────────────────────────────────────────────────────
test("style.css defines the image viewer layout + lightbox", () => {
assert.match(css, /\.image-stage\s*\{/);
diff --git a/tests/frontend/unit.test.mjs b/tests/frontend/unit.test.mjs
index 1d1e1a1..b5e284b 100644
--- a/tests/frontend/unit.test.mjs
+++ b/tests/frontend/unit.test.mjs
@@ -240,6 +240,50 @@ function testAIFabModuleExports() {
console.log(' ✓ ai-fab.js exports initAIFab');
}
+// ── Test header trimming + sidebar account section (#112) ──────────────────
+const AUTH_PATH = join(JS_DIR, 'auth.js');
+const STYLE_PATH = join(FRONTEND_DIR, 'style.css');
+
+function testSidebarUserSection() {
+ const html = readFileSync(INDEX_HTML, 'utf-8');
+ const auth = readFileSync(AUTH_PATH, 'utf-8');
+ const css = readFileSync(STYLE_PATH, 'utf-8');
+
+ // The header no longer carries the user block / version badge…
+ assert.doesNotMatch(html, /id="user-menu"/, 'header #user-menu must be removed');
+ assert.doesNotMatch(html, /id="logout-btn"/, 'header sign-out button must be removed');
+ const headerRight = html.slice(
+ html.indexOf('class="header-right"'),
+ html.indexOf('class="header-menu"'),
+ );
+ assert.doesNotMatch(headerRight, /version-badge/, 'version badge must leave the header-right');
+ assert.doesNotMatch(headerRight, /user-menu/, 'user block must leave the header-right');
+
+ // …the version lives in the options dropdown…
+ assert.match(html, /class="menu-list-row menu-list-version"[\s\S]*?id="version-badge"/);
+
+ // …and the account section sits at the bottom of the sidebar.
+ assert.match(html, /id="sidebar-user"/);
+ assert.match(html, /id="sidebar-user-logout"/);
+ assert.match(html, /id="sidebar-user-name"/);
+ assert.match(html, /id="sidebar-user-avatar"/);
+
+ // auth.js drives it.
+ assert.match(auth, /renderUserSection\(\)/);
+ assert.match(auth, /getElementById\("sidebar-user"\)/);
+
+ // styles + i18n exist in FR and EN.
+ assert.match(css, /\.sidebar-user\s*\{/);
+ assert.match(css, /\.menu-list-version\s*\{/);
+ for (const lang of ['fr', 'en']) {
+ const locale = JSON.parse(readFileSync(join(FRONTEND_DIR, 'locales', `${lang}.json`), 'utf-8'));
+ for (const key of ['header.menu_version', 'sidebar.user_role_admin', 'sidebar.user_role_user']) {
+ assert.ok(locale[key], `${lang}: missing i18n key ${key}`);
+ }
+ }
+ console.log(' ✓ header trimmed + sidebar account section (#112)');
+}
+
// ── Run all tests ──────────────────────────────────────────────────────────
async function main() {
let passed = 0, failed = 0;
@@ -255,6 +299,7 @@ async function main() {
['ai-fab module exists', testAIFabModuleExists],
['ai-fab module exports', testAIFabModuleExports],
['config TOC icons', testConfigTocIcons],
+ ['sidebar user section', testSidebarUserSection],
];
for (const [name, fn] of tests) {