diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5ed14..63f2baa 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.39.10**. +> [Unreleased](#unreleased). La dernière version livrée est **2.40.0**. --- @@ -14,6 +14,27 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.40.0] — 2026-09-29 + +### Ajouté + +- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 1) : ruban de commandes groupé, + onglets de feuilles permanents avec bouton « + », badges d'état.** + La vue tableur gagne une barre de commandes segmentée (Formules · Insertion · Vue · + Fichier) avec un bouton **Enregistrer** primaire. La barre d'onglets est désormais + toujours affichée (même à une seule feuille) et un bouton « + » y ajoute une feuille + (même pipeline `PUT …/xlsx/structure`, re-rendu depuis le serveur). Deux pastilles + d'état annoncent les limites de la vue : **lecture seule** (`.xls`/`.ods` — plus de + « + », ni de structure, ni de tableau de bord, ni d'édition) et **formules non + recalculées**. Des tokens de grille dédiés (`--grid-bg`, `--grid-header-bg`, + `--grid-header-text`, `--grid-border`, `--grid-zebra`, déclinés dark/light) rendent les + en-têtes clairement distincts des cellules, avec zébrage, survol et cellule active + renforcée. Audit UX, architecture cible et plan par lots : + [docs/features/xlsx-ui-redesign.md](docs/features/xlsx-ui-redesign.md). Tests JSDOM + `tests/frontend/xlsx-viewer.test.mjs` (41, dont 6 nouveaux). Aucun changement backend. + +--- + ## [2.39.10] — 2026-09-29 ### Sécurité diff --git a/README.fr.md b/README.fr.md index 2baed53..59004c5 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. -[![Version](https://img.shields.io/badge/Version-2.39.10-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.40.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -976,8 +976,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.39.10). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.40.0). --- -*Projet : ObsiGate | Version : 2.39.10 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.40.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 0ff923b..768c4db 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. -[![Version](https://img.shields.io/badge/Version-2.39.10-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.40.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1151,8 +1151,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.39.10). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.40.0). --- -*Project: ObsiGate | Version: 2.39.10 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.40.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 44cc0ba..770060b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.39.10 +2.40.0 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 5b74e87..dc84c8b 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.39.10" +version = "2.40.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index cb242bf..3489b66 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.39.10" +version = "2.40.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 d99ad5a..ccfcc24 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.39.10", + "version": "2.40.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md b/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md index 868947e..aea9b23 100644 --- a/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md +++ b/docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md @@ -136,13 +136,22 @@ curl "http://localhost:2020/api/file/Recettes/pdf/info?path=menu.pdf" ### Affichage et édition Un fichier `.xlsx` s'ouvre dans une visionneuse dédiée : un tableau par -feuille, des onglets pour naviguer entre elles, les en-têtes A1/B1 et les -numéros de ligne. Chaque cellule est modifiable directement (clic), `Entrée` -valide, `Échap` annule la saisie. **Enregistrer** envoie les cellules -modifiées à `PUT /api/file/{vault}/xlsx/save` : une sauvegarde par feuille, -avec **backup automatique** du fichier avant écriture, et une écriture +feuille, des onglets pour naviguer entre elles (toujours visibles, même à +une seule feuille), les en-têtes A1/B1 et les numéros de ligne. La barre de +commandes regroupe les actions en sections (Formules · Insertion · Vue · +Fichier) autour d'un bouton **Enregistrer** principal. Chaque cellule est +modifiable directement (clic), `Entrée` valide, `Échap` annule la saisie. +**Enregistrer** envoie les cellules modifiées à +`PUT /api/file/{vault}/xlsx/save` : une sauvegarde par feuille, avec +**backup automatique** du fichier avant écriture, et une écriture **atomique** (le classeur n'est jamais laissé à moitié écrit). +Le bouton **« + »** à côté des onglets ajoute une nouvelle feuille. Deux +pastilles d'état rappellent les limites de la vue : **« Lecture seule »** +pour les formats `.xls`/`.ods`, et **« Formules non recalculées »** — ObsiGate +affiche la formule telle qu'elle est enregistrée, Excel la recalcule à +l'ouverture et les cellules dépendantes ne se rafraîchissent pas à l'écran. + ### Avertissement avant enregistrement Certains classeurs contiennent des éléments qu'ObsiGate ne sait pas diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 60ea56f..ad915b6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.39.10 | **Dernière mise à jour :** 2026-09-29 +> **Version :** 2.40.0 | **Dernière mise à jour :** 2026-09-29 > **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)** @@ -92,6 +92,30 @@ --- +## 🔵 En cours — Refonte UI/UX tableur (P2) + +### 154. Refonte UI/UX de la visionneuse & éditeur XLSX (ruban, grille, inspecteur) + +- **Effort :** 6-9 jours (Lot 1 ✅ · Lot 2 · Lot 3 · Lot 4) | **Impact :** 🟡 +- **Statut :** 🔵 **en cours** — **Lot 1 livré le 2026-09-29** (ruban de commandes groupé, onglets + de feuilles permanents avec bouton « + », badges d'état lecture seule / formules non + recalculées, tokens de grille et affordances). Lots suivants : dialogues thémés, inspecteur + droit (dashboard + IA), découpage modulaire + undo/redo. +- **Analyse, architecture cible et plan par lots :** [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) +- **Description :** la visionneuse XLSX (#152/#153) est fonctionnelle mais peu conviviale : + commandes à plat sans hiérarchie, en-têtes de grille indistincts des cellules, états avancés + (tableau de bord, troncature, lecture seule, formules non recalculées, conflits) mal intégrés. + La refonte s'appuie sur les standards Excel/Google Sheets/Airtable **sans renier** la contrainte + `vanilla JS`, zéro framework, zéro build npm. +- **Sous-tâches :** + - [x] **A1** Coquille : barre de commandes groupée, onglets feuilles permanents + « + », + badges d'état, tokens de grille et affordances visuelles (Lot 1) + - [ ] **A2** Dialogues thémés (modales + toasts) et feedback non bloquant des conflits 409 (Lot 2) + - [ ] **A3** Inspecteur droit repliable : Tableau de bord + Assistant IA, lien dashboard ⇄ grille (Lot 3) + - [ ] **A4** Découpage `frontend/js/xlsx/*` + undo/redo + défilement via `IntersectionObserver` (Lot 4) + +--- + ## ⚪ Backlog — Priorité 4 (P4) ### 73. Synchronisation multi-appareils — Obsidian Sync compatible @@ -219,7 +243,8 @@ | ⚪ P4 reporté | #73 Sync — **reporté (décision 2026-09-26)**, hors chemin critique | 6-8 jours si réactivé | | ⚪ P0/P1 prioritaire | #87 CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~3-5 jours | | ✅ Terminé | #153 Visionneuse & édition XLSX — complétude (A1-A17 **toutes livrées**, v2.27.0 → v2.39.0) | 0 jour restant | -| **Total chemin critique** | **#77 fin + #87** | **~4-6 jours** | +| 🔵 En cours | #154 Refonte UI/UX tableur (A1 ✅ Lot 1 · A2-A4 restants) | ~5-7 jours | +| **Total chemin critique** | **#77 fin + #87 + #154** | **~9-13 jours** | --- @@ -227,6 +252,7 @@ - **Décisions 2026-09-26 :** axe prioritaire = dette & sécurité (#85/#87) ; #73 Sync reporté (P4, hors chemin critique) ; desktop livré non signé + doc SmartScreen. - **Ajout 2026-09-27 :** #153 ouvert à la suite de l'audit de la visionneuse XLSX (limitations, risques de perte de données, périmètre IA/recherche) — détail et critères dans [features/xlsx-viewer.md](./features/xlsx-viewer.md). +- **Ajout 2026-09-29 :** #154 ouvert — refonte UI/UX de la visionneuse/éditeur XLSX (audit UX, architecture cible, plan par lots) dans [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) ; Lot 1 livré (ruban groupé, onglets permanents + « + », badges d'état, tokens de grille). - **Clôture #85 (v2.27.13) :** monolithe découpé (T1→T9), stores verrouillés + rate-limit SQLite (T10), fiche `docs/features/archi-refonte-85.md`. - Les items P3/P4 ne sont pas ordonnés par priorité interne — à raffiner selon les retours utilisateurs. - L'effort inclut le développement + tests unitaires + intégration CI, mais pas la documentation utilisateur. diff --git a/docs/features/xlsx-ui-redesign.md b/docs/features/xlsx-ui-redesign.md new file mode 100644 index 0000000..f2623c7 --- /dev/null +++ b/docs/features/xlsx-ui-redesign.md @@ -0,0 +1,118 @@ +# #154 — Refonte UI/UX de la visionneuse & éditeur XLSX (ruban, grille, inspecteur) + +> **Item de roadmap :** [#154 — Refonte UI/UX tableur](../ROADMAP.md) +> **Origine :** #152 / #153 (visionneuse XLSX fonctionnelle mais peu conviviale) +> **Statut :** 🔵 **en cours** — Lot 1 livré le 2026-09-29 +> **Effort estimé :** 6-9 jours (Lot 1 ✅ · Lot 2 · Lot 3 · Lot 4) +> **Règle de maintenance :** la Roadmap porte les cases à cocher (suivi), cette fiche porte +> l'analyse, l'architecture cible et le plan par lots. **Ne pas dupliquer le détail.** + +--- + +## 1. Objectif + +Rendre la vue tableur d'ObsiGate **intuitive, moderne et hautement utilisable** en s'inspirant +des standards du marché (Excel, Google Sheets, Airtable), **sans renier les contraintes du +dépôt** : thème sombre, `vanilla JS`, **zéro framework, zéro build npm** +([`AGENTS.md`](../../AGENTS.md)). La refonte est **organique** : on améliore la coquille +existante (`frontend/js/viewer.js::renderXlsxViewer`, `frontend/style.css`), on ne réécrit pas +la grille ni le backend. + +## 2. Audit UX — les 3 problèmes majeurs + +| # | Problème | Constat | Résolution | +|---|---|---|---| +| **P1** | **Aucune hiérarchie ni regroupement des commandes** | Rangée plate de boutons de poids identique (`viewer.js` toolbar historique) ; « Tableau de bord » *prependé* au runtime ; barre de formule réduite à un `input`. | **Barre de commandes groupée** (Formules · Insertion · Vue · Fichier), bouton **Enregistrer primaire**, état *dirty*. | +| **P2** | **Grille sans affordances : en-têtes = cellules** | Contraste faible entre `th` et `td`, pas de zébrage, pas de survol lisible, cellule active peu marquée. | **Tokens de grille** + en-têtes plus clairs/interactifs, zébrage, survol, cellule active en bordure accent. | +| **P3** | **États avancés traités comme du contenu** | Dashboard *inline* qui pousse la grille, troncature/lecture seule/formules non recalculées sans emplacement dédié, `confirm()`/`prompt()` natifs. | **Couche UI dédiée** : bandeaux d'état + **inspecteur droit** (Lot 3) + dialogues thémés (Lot 2). | + +## 3. Architecture cible de l'écran + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ BARRE APP (globale, existante) │ +├─────────────┬────────────────────────────────────────────────────────────┤ +│ │ A. RUBAN — groupes Formules · Insertion · Vue · Fichier │ +│ EXPLORATEUR│ B. BARRE DE FORMULE — [ A1 ] fx [ … ] │ +│ DE FICHIERS│ C. BANDEAUX D'ÉTAT — lecture seule · formules non recalculées│ +│ (sidebar) ├──────────────────────────────────────────────┬─────────────┤ +│ │ D. GRILLE (en-têtes clairs, zébrage, survol) │ E. INSPECTEUR│ +│ │ │ (dashboard + │ +│ │ │ IA, repliable)│ +│ ├───────────────────────────────────────────────┤ │ +│ │ F. ONGLETS FEUILLES + « + » · 500/522 │ │ +└─────────────┴───────────────────────────────────────────────┴─────────────┘ +``` + +- **A. Ruban** : groupes d'actions avec séparateurs ; actions de style désactivées (styles lus, + pas écrits). Bouton **Enregistrer** en accent, désactivé si rien de *dirty*. +- **B. Barre de formule** : zone nom + champ + badge de session `f(x)`. +- **C. Bandeaux d'état** : empilables, non bloquants ; portent lecture seule et + « formules non recalculées ». +- **D. Grille** : rendue côté serveur (`backend/xlsx_reader.py`), habillée et câblée par le front. +- **E. Inspecteur** : **à venir (Lot 3)** — Tableau de bord + Assistant IA dans un panneau droit + repliable (réutilise `PaneManager` pour le détachement), au lieu du dashboard *inline* actuel. +- **F. Onglets feuilles** : permanents (même à une seule feuille) + bouton « + ». + +## 4. Plan par lots (incréments livrables) + +### Lot 1 — Coquille : ruban groupé, onglets permanents, badges d'état ✅ *(2026-09-29)* + +- **A1.1** Barre de commandes groupée (`.xlsx-cmdbar`, `.xlsx-cmd-group`, `.xlsx-cmd-sep`, + `.xlsx-save-primary`), IDs existants conservés (compatibilité tests JSDOM/E2E). +- **A1.2** Onglets de feuilles **toujours rendus** (non-CSV) + bouton **`+`** `.xlsx-tab-add` + → `sheet_add` (même pipeline `putStructure`). +- **A1.3** Badges d'état : `.xlsx-status-pill` **lecture seule** (`.xls`/`.ods`) et + **formules non recalculées** (non-CSV). +- **A1.4** Tokens de grille (`--grid-bg`, `--grid-header-bg`, `--grid-header-text`, + `--grid-border`, `--grid-zebra`) déclinés dark/light + affordances (en-têtes clairs, + zébrage, survol, cellule active solide, cellule *dirty* prioritaire au survol). + +### Lot 2 — Dialogues thémés & feedback (à venir) + +- Remplacer `confirm()`/`prompt()` par des **modales thémées** (`.obsigate-modal-*`) et des + **toasts** : structure (feuille/ligne/colonne), perte (409 `xlsx_lossy_content`), conflit + (409 `conflict`) en bandeau non bloquant. +- Indicateur *dirty* sur l'onglet de feuille et la barre. + +### Lot 3 — Inspecteur droit (à venir) + +- Panneau repliable hébergeant **Tableau de bord** et **Assistant IA** en onglets ; la grille + reste visible. Lien **dashboard → grille** (clic sur KPI/plage = scroll + sélection). +- Détachement en split via `PaneManager.splitRight()`. + +### Lot 4 — Découpage & finitions (à venir) + +- Extraction de `renderXlsxViewer` en modules (`frontend/js/xlsx/*.js`) : `toolbar`, `formula-bar`, + `grid`, `sheet-tabs`, `inspector`, `cell-editor`, `api`. +- **Undo/redo** local (pile de commandes), défilement paresseux via `IntersectionObserver`, + accessibilité ARIA (`role="grid"`, *roving tabindex*). + +## 5. Recommandations techniques (contrainte « zéro build ») + +| Option Data Grid | Build | Licence | Verdict | +|---|---|---|---| +| AG Grid Community | npm + bundler | MIT | ❌ viole « zéro build », réécrit le DOM, casse les tests | +| Handsontable | npm + bundler | **commerciale** | ❌ licence non libre | +| TanStack Table | headless (importable esm.sh) | MIT | ⚠ possible sans build, mais *headless* → gain limité | +| **Grille maison sur ``** | aucun | — | ✅ **recommandé** (conserve DOM, CSP, i18n, tests) | + +- **Performance** : ne pas ré-écrire tout le DOM ; réutiliser le pipeline `appendWindow` ; + `content-visibility:auto; contain:strict` sur les lignes ; garder la pagination serveur + (500 × 40 = 20 000 cellules/feuille) plutôt qu'une virtualisation client complexe. +- **CSP** : `main.py` autorise déjà `esm.sh` — une lib *headless* reste possible en Lot 4 si + un vrai besoin de modèle de colonnes apparaît. + +## 6. Critères d'acceptation (par lot) + +- **Lot 1** : une feuille unique affiche son onglet + « + » ; « + » ajoute une feuille via + `PUT …/xlsx/structure` et re-rend ; `.xls`/`.ods` montrent le badge « lecture seule » (pas de + « + », pas de structure, pas de dashboard) ; un `.xlsx` montre le badge « formules non + recalculées », un `.csv` non ; les tests JSDOM/E2E existants restent verts + nouveaux tests. +- Lots suivants : définis à leur ouverture. + +## 7. Historique + +| Date | Événement | +|---|---| +| 2026-09-29 | Audit UX (3 problèmes) + architecture cible + plan par lots ; **Lot 1** livré (ruban groupé, onglets permanents + « + », badges d'état, tokens de grille) | diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index 98b61db..04ce807 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -1047,11 +1047,18 @@ export function renderXlsxViewer(area, data) { // per workbook, not a global preference). let lossyConfirmed = false; let allowFormula = false; - const tabs = sheets.length > 1 - ? `
${sheets.map((s, i) => - `` - ).join("")}
` - : ""; + // #154-A1 — the command bar is grouped (Formules · Insertion · Vue · Fichier) + // and the sheet tabs are ALWAYS rendered (a single sheet used to show no tab + // at all, hiding the fact that a workbook can hold several). An editable + // workbook also gets an explicit “+” button next to the tabs. + const editable = !isCsv && !readOnly; + const tabs = isCsv + ? "" + : `
${sheets.map((s, i) => + `` + ).join("")}${editable + ? `` + : ""}
`; // #153 A15 — inline styles (bold/italic/colors from the file itself), merged // ranges and the freeze anchor are applied AFTER the panels are in the DOM // (the HTML string alone cannot carry them: col/row spans need the table). @@ -1123,24 +1130,55 @@ export function renderXlsxViewer(area, data) { ` : ""; + // #154-A1 — status pills make the viewer's limits visible up front: a + // read-only format (.xls/.ods) and the fact that formulas are never + // recalculated on screen (the workbook is only written back, Excel recomputes). + const statusPills = [ + readOnly + ? `${escapeHtml(t("xlsx.readonly_badge"))}` + : "", + !isCsv + ? `${escapeHtml(t("xlsx.formulas_note"))}` + : "", + ].filter(Boolean).join(""); + const statusBar = statusPills ? `
${statusPills}
` : ""; + + // Command groups, separated by thin rules. The dashboard button is appended + // later into the dedicated (empty) “view” group. + const actionGroups = []; + if (!readOnly) { + actionGroups.push(` + + `); + } + if (editable) { + actionGroups.push(` + + `); + actionGroups.push(``); + } + actionGroups.push(` + + + + `); + const actionsHtml = actionGroups.join(''); + area.innerHTML = `
-
- ${tabs} - - - - - - ${isCsv || readOnly ? "" : ``} - + ${lossWarning}
@@ -1173,7 +1211,7 @@ export function renderXlsxViewer(area, data) { type: "button", title: t("xlsx.dashboard_btn"), }, [icon("layout-dashboard", 14), document.createTextNode(t("xlsx.dashboard_btn"))]); - area.querySelector(".xlsx-toolbar-actions").prepend(dashBtn); + (area.querySelector("#xlsx-view-group") || area.querySelector(".xlsx-toolbar-actions")).appendChild(dashBtn); dashBtn.addEventListener("click", async () => { const old = area.querySelector(".xlsx-dashboard"); if (old) { old.remove(); dashBtn.classList.remove("active"); return; } @@ -1482,8 +1520,9 @@ export function renderXlsxViewer(area, data) { }); // Formula toggle (#153 A4) — opt-in for this viewing session only. + // #154-A1 — absent on a read-only workbook (nothing can be saved). const formulaBtn = area.querySelector("#xlsx-formula-btn"); - formulaBtn.addEventListener("click", () => { + if (formulaBtn) formulaBtn.addEventListener("click", () => { allowFormula = !allowFormula; formulaBtn.setAttribute("aria-pressed", String(allowFormula)); formulaBtn.classList.toggle("active", allowFormula); @@ -1749,6 +1788,16 @@ export function renderXlsxViewer(area, data) { return null; }; + // #154-A1 — the “+” button mirrors the structure menu's « Ajouter une + // feuille » item, right where the tabs are (Excel-like). + const tabAddBtn = area.querySelector("#xlsx-tab-add"); + if (tabAddBtn) tabAddBtn.addEventListener("click", async () => { + const name = prompt(t("xlsx.structure_prompt_add")); + if (!name) return; + const actions = [{ op: "sheet_add", name }]; + try { await putStructure(actions); } catch (err) { structureError(err, actions); } + }); + // Structure menu: built on demand, positioned under the button. // (.csv / read-only workbooks have no such button — the menu block above // is skipped for them.) diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 82bf57f..f2ebfa0 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -1829,6 +1829,11 @@ "xlsx.lossy_cancelled": "Save cancelled", "xlsx.formula_toggle_title": "Treat “=” and “@” as formulas (off by default)", "xlsx.cached_value_title": "Last value calculated by Excel", + "xlsx.tabs_add_sheet": "Add a sheet", + "xlsx.readonly_badge": "Read-only", + "xlsx.readonly_hint": "This format (.xls / .ods) cannot be edited in ObsiGate — convert it to .xlsx to edit.", + "xlsx.formulas_note": "Formulas not recalculated", + "xlsx.formulas_note_title": "ObsiGate shows the formula as stored: Excel recalculates it on open. Dependent cells do not refresh on screen.", "xlsx.truncated_title": "Truncated sheet", "xlsx.truncated_rows": "{shown} of {total} rows displayed.", "xlsx.truncated_cols": "{shown} of {total} columns displayed.", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 4009b8d..b1c14fd 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -1829,6 +1829,11 @@ "xlsx.lossy_cancelled": "Sauvegarde annulée", "xlsx.formula_toggle_title": "Interpréter « = » et « @ » comme des formules (désactivé par défaut)", "xlsx.cached_value_title": "Dernière valeur calculée par Excel", + "xlsx.tabs_add_sheet": "Ajouter une feuille", + "xlsx.readonly_badge": "Lecture seule", + "xlsx.readonly_hint": "Ce format (.xls / .ods) n'est pas modifiable dans ObsiGate — convertissez-le en .xlsx pour l'éditer.", + "xlsx.formulas_note": "Formules non recalculées", + "xlsx.formulas_note_title": "ObsiGate affiche la formule telle qu'elle est enregistrée : Excel la recalcule à l'ouverture. Les cellules dépendantes ne se rafraîchissent pas à l'écran.", "xlsx.truncated_title": "Feuille tronquée", "xlsx.truncated_rows": "{shown} lignes affichées sur {total}.", "xlsx.truncated_cols": "{shown} colonnes affichées sur {total}.", diff --git a/frontend/style.css b/frontend/style.css index 7ec4b74..88eafeb 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -57,6 +57,13 @@ --mono: "JetBrains Mono", monospace; --radius: 6px; --radius-lg: 10px; + /* #154-A1 — data-grid tokens: the sheet reads slightly lighter than the + chrome (toolbar/panels), headers are clearly distinct from cells. */ + --grid-bg: #161b22; + --grid-header-bg: #1f2430; + --grid-header-text: #e6edf3; + --grid-border: #30363d; + --grid-zebra: rgba(255, 255, 255, 0.025); } /* ===== THEME — DARK (explicit) ===== */ @@ -101,6 +108,11 @@ --mono: "JetBrains Mono", monospace; --radius: 6px; --radius-lg: 10px; + --grid-bg: #161b22; + --grid-header-bg: #1f2430; + --grid-header-text: #e6edf3; + --grid-border: #30363d; + --grid-zebra: rgba(255, 255, 255, 0.025); } /* ===== THEME — LIGHT ===== */ @@ -145,6 +157,11 @@ --mono: "JetBrains Mono", monospace; --radius: 6px; --radius-lg: 10px; + --grid-bg: #ffffff; + --grid-header-bg: #eaeef2; + --grid-header-text: #1f2328; + --grid-border: #d0d7de; + --grid-zebra: rgba(0, 0, 0, 0.025); } /* ===== BASE ===== */ @@ -10925,21 +10942,54 @@ body.desktop-mode .editor-container { } /* ── XLSX Viewer ── */ +/* #154-A1 — the viewer shell is a grouped command bar: sheet tabs on the left, + action groups (Formules · Insertion · Vue · Fichier) on the right, then a + status row stating the viewer's limits (read-only, formulas not recalculated). */ .xlsx-toolbar { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 8px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface2); + position: relative; /* anchors the A14 structure menu */ +} +.xlsx-cmdbar { display: flex; align-items: center; gap: 10px; - margin-bottom: 8px; flex-wrap: wrap; - position: relative; /* anchors the A14 structure menu */ } .xlsx-toolbar-actions { margin-left: auto; display: flex; - gap: 8px; + align-items: center; + gap: 6px; +} +.xlsx-cmd-group { + display: inline-flex; + align-items: center; + gap: 6px; +} +.xlsx-cmd-sep { + width: 1px; + height: 20px; + flex: 0 0 auto; + background: var(--border); +} +.xlsx-save-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} +.xlsx-save-primary:disabled { + opacity: 0.5; } .xlsx-tabs { display: flex; + align-items: center; gap: 4px; flex-wrap: wrap; } @@ -10957,15 +11007,73 @@ body.desktop-mode .editor-container { border-color: var(--accent, #4a90d9); color: #fff; } +.xlsx-tab-add { + border: 1px dashed var(--border); + background: transparent; + color: var(--text-secondary); + border-radius: 4px; + padding: 4px 10px; + font-size: 0.9rem; + line-height: 1; + cursor: pointer; +} +.xlsx-tab-add:hover { + border-color: var(--accent); + color: var(--accent); +} +.xlsx-status-bar { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} +.xlsx-status-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 0.72rem; + white-space: nowrap; +} +.xlsx-status-icon { + width: 12px; + height: 12px; + flex: 0 0 auto; +} +.xlsx-status-readonly { + border-color: var(--warning, #e0a800); + color: var(--warning, #e0a800); +} +.xlsx-status-formula { + color: var(--accent); +} +.xlsx-table { + background: var(--grid-bg); +} +.xlsx-table td { + border-bottom: 1px solid var(--grid-border); +} +/* #154-A1 — zebra + hover make rows scannable; headers use dedicated tokens so + they are visually distinct from the cells. */ +.xlsx-table tbody tr:nth-child(even) td { + background: var(--grid-zebra); +} +.xlsx-table tbody tr:hover td { + background: var(--bg-hover); +} .xlsx-table th.xlsx-corner, .xlsx-table th.xlsx-rownum { - background: var(--surface); - color: var(--text-secondary); - font-weight: 400; + background: var(--grid-header-bg); + color: var(--grid-header-text); + font-weight: 500; text-align: right; padding: 6px 8px; - border-bottom: 2px solid var(--border); - border-right: 1px solid var(--border-light, var(--border)); + border-bottom: 2px solid var(--grid-border); + border-right: 1px solid var(--grid-border); position: sticky; left: 0; /* #153 A8 — `top: auto` is load-bearing: `.csv-table th` pins EVERY `th` @@ -10986,7 +11094,9 @@ body.desktop-mode .editor-container { position: sticky; top: 0; z-index: 3; - background: var(--surface); + background: var(--grid-header-bg); + color: var(--grid-header-text); + border-bottom: 2px solid var(--grid-border); } .xlsx-table td[contenteditable] { cursor: text; @@ -10998,9 +11108,10 @@ body.desktop-mode .editor-container { outline-offset: -2px; } /* #153 A7 — the active cell keeps its outline even when focus moves to the - formula bar, so the user never loses track of what the bar edits. */ + formula bar, so the user never loses track of what the bar edits. + #154-A1 — solid (not dashed) for a stronger active-cell affordance. */ .xlsx-table td.xlsx-active:not(:focus) { - outline: 2px dashed var(--accent, #4a90d9); + outline: 2px solid var(--accent, #4a90d9); outline-offset: -2px; } @@ -11099,12 +11210,12 @@ body.desktop-mode .editor-container { position: sticky; top: 33px; /* thead height — keeps the frozen row below the header */ z-index: 2; - background: var(--surface); + background: var(--grid-header-bg); } .xlsx-table td.xlsx-frozen-col { position: sticky; left: 44px; /* the row-number column width */ - background: var(--surface); + background: var(--grid-bg); } /* #153 A14 — structure menu (sheets / rows / columns) */ @@ -11232,7 +11343,9 @@ body.desktop-mode .editor-container { mark { font: inherit; } -.xlsx-table td.xlsx-dirty { +/* #154-A1 — scoped to tbody so a dirty cell keeps its highlight even on the + zebra/hover backgrounds (specificity beats the zebra + hover rules). */ +.xlsx-table tbody td.xlsx-dirty { background: rgba(255, 196, 0, 0.18); } diff --git a/package.json b/package.json index 4448b40..176efef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.39.10", + "version": "2.40.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/frontend/xlsx-viewer.test.mjs b/tests/frontend/xlsx-viewer.test.mjs index 728b51f..44236d0 100644 --- a/tests/frontend/xlsx-viewer.test.mjs +++ b/tests/frontend/xlsx-viewer.test.mjs @@ -718,6 +718,88 @@ await test("two windows in a row walk the whole sheet", async () => { assert.equal(area.querySelector(".xlsx-load-more").classList.contains("done"), true); }); +// ── #154-A1 — command bar, sheet tabs, status pills ───────────────────────── + +await test("a single-sheet workbook still shows its tab and a + button", () => { + const area = mount(); + assert.ok(area.querySelector(".xlsx-tabs"), "the tab strip is always rendered"); + assert.equal(area.querySelectorAll(".xlsx-tab").length, 1); + const add = area.querySelector("#xlsx-tab-add"); + assert.ok(add, "the + button is present for an editable sheet"); + assert.equal(add.getAttribute("title"), FR["xlsx.tabs_add_sheet"]); +}); + +await test("a csv shows no tab strip and no + button", () => { + const area = document.getElementById("content-area"); + area.innerHTML = ""; + renderXlsxViewer(area, { + vault: "V", path: "data.csv", is_csv: true, + xlsx_sheets: [{ name: "Feuille1", html: sheetHtml("100") }], + xlsx_lossy_features: [], + }); + assert.equal(area.querySelector(".xlsx-tabs"), null); + assert.equal(area.querySelector("#xlsx-tab-add"), null); +}); + +await test("the + button adds a sheet through the structure endpoint", async () => { + const area = mount(); + const realPrompt = globalThis.prompt; + globalThis.prompt = () => "Feuille 2"; + apiQueue.push({ ok: true, status: 200, body: { status: "ok" } }); // PUT + apiQueue.push({ + ok: true, status: 200, + body: { is_xlsx: true, vault: "V", path: "data.xlsx", xlsx_sheets: [{ name: "Feuille 2", html: sheetHtml("neuf") }], xlsx_lossy_features: [] }, + }); // re-read + area.querySelector("#xlsx-tab-add").click(); + await new Promise((r) => setTimeout(r, 5)); + globalThis.prompt = realPrompt; + assert.equal(calls.length, 2); + assert.match(calls[0].url, /\/xlsx\/structure\?path=data\.xlsx/); + assert.deepEqual(calls[0].body.actions, [{ op: "sheet_add", name: "Feuille 2" }]); + assert.equal(area.querySelector('td[data-cell="A1"]')?.textContent, "neuf"); +}); + +await test("a read-only workbook shows the read-only pill and offers no editing", () => { + const area = document.getElementById("content-area"); + area.innerHTML = ""; + renderXlsxViewer(area, { + vault: "V", path: "data.xls", is_xlsx: true, xlsx_readonly: true, + xlsx_sheets: [{ name: "Feuille1", html: sheetHtml("100") }], + xlsx_lossy_features: [], + }); + const pill = area.querySelector(".xlsx-status-readonly"); + assert.ok(pill, "read-only pill"); + assert.ok(pill.textContent.includes(FR["xlsx.readonly_badge"]), pill.textContent); + assert.equal(area.querySelector("#xlsx-tab-add"), null, "no + for read-only"); + assert.equal(area.querySelector("#xlsx-structure-btn"), null, "no structure for read-only"); + assert.equal(area.querySelector('td[data-cell="A1"]').getAttribute("contenteditable"), null, "cells are not editable"); +}); + +await test("the formulas-not-recalculated pill is shown for xlsx but not csv", () => { + const area = mount(); + const pill = area.querySelector(".xlsx-status-formula"); + assert.ok(pill, "formula pill on xlsx"); + assert.ok(pill.textContent.includes(FR["xlsx.formulas_note"]), pill.textContent); + + const csvArea = document.getElementById("content-area"); + csvArea.innerHTML = ""; + renderXlsxViewer(csvArea, { + vault: "V", path: "data.csv", is_csv: true, + xlsx_sheets: [{ name: "Feuille1", html: sheetHtml("100") }], + xlsx_lossy_features: [], + }); + assert.equal(csvArea.querySelector(".xlsx-status-formula"), null); +}); + +await test("the command bar exposes grouped actions with a primary save", () => { + const area = mount(); + assert.ok(area.querySelector(".xlsx-cmdbar"), "grouped command bar"); + assert.ok(area.querySelector('#xlsx-save-btn').classList.contains("xlsx-save-primary")); + assert.ok(area.querySelector(".xlsx-cmd-group[data-group='formulas']")); + assert.ok(area.querySelector(".xlsx-cmd-group[data-group='insert']")); + assert.ok(area.querySelector(".xlsx-cmd-group[data-group='file']")); +}); + // ── Report ────────────────────────────────────────────────────────────────── console.log(`\n${passCount}/${testCount} tests passed\n`); process.exit(passCount === testCount ? 0 : 1);