Compare commits

..
2 Commits
Author SHA1 Message Date
bruno 72da123a51 feat: undo/redo, chargement IntersectionObserver et ARIA pour la visionneuse XLSX #154
CI / lint (push) Successful in 2m30s
CI / security (push) Failing after 1m45s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / test (push) Canceled after 19s
2026-09-29 15:01:00 -04:00
bruno e94af0369b feat: inspecteur droit pour le tableau de bord XLSX #154
CI / lint (push) Successful in 2m30s
CI / security (push) Failing after 1m46s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / test (push) Canceled after 1h32m51s
2026-09-29 14:56:08 -04:00
16 changed files with 355 additions and 42 deletions
+29 -1
View File
@@ -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.41.0**.
> [Unreleased](#unreleased). La dernière version livrée est **2.43.0**.
---
@@ -14,6 +14,34 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.43.0] — 2026-09-29
### Ajouté
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 4) : undo/redo, défilement et accessibilité.**
Les éditions de cellules peuvent être **annulées / rétablies** (boutons dans le ruban,
raccourcis `Ctrl+Z`, `Ctrl+Maj+Z`, `Ctrl+Y`). Les fenêtres de lignes d'une feuille tronquée se
chargent via **`IntersectionObserver`** (repli sur l'écouteur de défilement quand l'API est
absente). La grille expose désormais une sémantique **ARIA** (`role="grid"` / `row` /
`gridcell` / `columnheader` / `rowheader`). Tests JSDOM `tests/frontend/xlsx-viewer.test.mjs`
(47) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts. Aucun changement backend.
---
## [2.42.0] — 2026-09-29
### Ajouté
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 3) : inspecteur droit.**
Le **Tableau de bord** quitte le flux de la grille pour un **panneau latéral droit repliable**
(`.xlsx-inspector`) : la grille reste visible à côté. L'en-tête de l'inspecteur porte le titre,
une entrée **Assistant IA** (ouvre le panneau latéral global existant) et un bouton de fermeture.
Sous 900 px, l'inspecteur se place sous la grille. Tests JSDOM
`tests/frontend/xlsx-viewer.test.mjs` (44) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts.
Aucun changement backend.
---
## [2.41.0] — 2026-09-29
### Ajouté
+3 -3
View File
@@ -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.41.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.43.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.41.0).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.43.0).
---
*Projet : ObsiGate | Version : 2.41.0 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.43.0 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -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.41.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.43.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.41.0).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.43.0).
---
*Project: ObsiGate | Version: 2.41.0 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.43.0 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.41.0
2.43.0
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.41.0"
version = "2.43.0"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.41.0"
version = "2.43.0"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.41.0",
"version": "2.43.0",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+8 -4
View File
@@ -215,6 +215,9 @@ A1 au-dessus du tableau ; `Tab`/`Maj+Tab` et les flèches circulent entre les
cellules, `Entrée` valide, `Maj+Entrée` insère un retour à la ligne, une
plage se copie telle quelle vers un tableur.
**Annuler / rétablir** — `Ctrl+Z` (ou le bouton **Annuler** du ruban) revient
sur les dernières éditions de cellules, `Ctrl+Maj+Z` / `Ctrl+Y` les rétablit.
**Tri, filtre, recherche, export** — chaque colonne se trie (ascendant /
descendant, info-bulle : le tri s'applique à l'affichage seul), les lignes
se filtrent, la recherche (`Ctrl+F` du panneau) va de correspondance en
@@ -236,13 +239,14 @@ en **lecture seule** ; un `.csv` s'ouvre dans la même grille et se réécrit
conformément à la RFC 4180 (les guillemets et séparateurs sont
échappés).
**Tableau de bord** — le bouton **Tableau de bord** liste les plages
**Tableau de bord** — le bouton **Tableau de bord** ouvre un **inspecteur
latéral droit** (la grille reste visible à côté) qui liste les plages
nommées du classeur (nom, référence, portée), signale les feuilles
contenant des graphiques ou des tableaux croisés, et donne pour chaque
feuille un résumé (cellules, lignes, colonnes, formules, valeurs
numériques) avec quelques chiffres clés. C'est une aide à la lecture :
les plages nommées peuvent ensuite être exploitées avec l'assistant IA
(outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
numériques) avec quelques chiffres clés. L'en-tête de l'inspecteur offre
aussi un accès direct à l'**assistant IA**, qui peut ensuite exploiter ces
plages (outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
`append_xlsx_rows`).
### Limites
+9 -6
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.41.0 | **Dernière mise à jour :** 2026-09-29
> **Version :** 2.43.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)**
@@ -97,11 +97,13 @@
### 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** — **Lots 1 & 2 livrés le 2026-09-29** (ruban de commandes groupé,
- **Statut :** 🔵 **en cours** — **Lots 1, 2, 3 & 4 livrés 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 ; dialogues thémés `showConfirm`/`showPrompt`,
bandeau de conflit 409 non bloquant, indicateur *dirty*). Lots suivants : inspecteur droit
(dashboard + IA), découpage modulaire + undo/redo.
bandeau de conflit 409 non bloquant, indicateur *dirty* ; **inspecteur droit repliable** pour le
tableau de bord avec entrée Assistant IA ; **undo/redo**, chargement via `IntersectionObserver`,
ARIA `role="grid"`). Reste **A5** : découpage modulaire (`frontend/js/xlsx/*`) et finitions
(lien dashboard → grille, inspecteur redimensionnable, split).
- **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
@@ -112,8 +114,9 @@
- [x] **A1** Coquille : barre de commandes groupée, onglets feuilles permanents + « + »,
badges d'état, tokens de grille et affordances visuelles (Lot 1)
- [x] **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)
- [x] **A3** Inspecteur droit repliable : Tableau de bord + entrée Assistant IA (Lot 3)
- [x] **A4** Undo/redo, chargement via `IntersectionObserver`, sémantique ARIA (Lot 4)
- [ ] **A5** Découpage `frontend/js/xlsx/*` + lien dashboard → grille, inspecteur redimensionnable, split
---
+23 -10
View File
@@ -2,8 +2,8 @@
> **Item de roadmap :** [#154 — Refonte UI/UX tableur](../ROADMAP.md)
> **Origine :** #152 / #153 (visionneuse XLSX fonctionnelle mais peu conviviale)
> **Statut :** 🔵 **en cours** — Lots 1 & 2 livrés le 2026-09-29
> **Effort estimé :** 6-9 jours (Lot 1 ✅ · Lot 2 ✅ · Lot 3 · Lot 4)
> **Statut :** 🔵 **en cours** — Lots 1, 2, 3 & 4 livrés le 2026-09-29 (reste A5, découpage modulaire)
> **Effort estimé :** 6-9 jours (Lot 1 ✅ · Lot 2 ✅ · Lot 3 ✅ · Lot 4 ✅ · Lot 5)
> **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.**
@@ -79,18 +79,29 @@ la grille ni le backend.
avec bouton **Réessayer** — les modifications sont conservées.
- **A2.4** Indicateur *dirty* sur le bouton **Enregistrer** et sur l'onglet de la feuille concernée.
### Lot 3 — Inspecteur droit (à venir)
### Lot 3 — Inspecteur droit ✅ *(2026-09-29)*
- 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()`.
- **A3.1** Le **Tableau de bord** quitte le flux de la grille pour un **panneau droit repliable**
(`.xlsx-inspector`) : la grille reste visible à côté (`.xlsx-body` = `.xlsx-main` + inspecteur).
- **A3.2** En-tête d'inspecteur : titre, bouton **Assistant IA** (ouvre le panneau latéral global
existant) et bouton de fermeture.
- **A3.3** Responsive : sous 900 px, l'inspecteur passe sous la grille.
### Lot 4 — Découpage & finitions (à venir)
### Lot 4 — Interactions : undo/redo, défilement, accessibilité ✅ *(2026-09-29)*
- **A4.1** **Undo/redo** local (pile de commandes) pour les éditions de cellules : boutons
**Annuler / Rétablir** dans le ruban + raccourcis `Ctrl+Z`, `Ctrl+Maj+Z`, `Ctrl+Y`.
- **A4.2** Chargement des fenêtres via **`IntersectionObserver`** (repli sur l'écouteur de
défilement pour les environnements sans IO).
- **A4.3** Sémantique **ARIA** : `role="grid"` / `row` / `gridcell` / `columnheader` / `rowheader`.
### Lot 5 — Découpage modulaire & 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*).
`grid`, `sheet-tabs`, `inspector`, `cell-editor`, `api` (refactor sans changement de
comportement, sous couvert des tests JSDOM/E2E existants).
- Lien **dashboard → grille** (clic sur KPI/plage = scroll + sélection), inspecteur
**redimensionnable**, détachement en split via `PaneManager.splitRight()`.
## 5. Recommandations techniques (contrainte « zéro build »)
@@ -121,3 +132,5 @@ la grille ni le backend.
|---|---|
| 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) |
| 2026-09-29 | **Lot 2** livré : dialogues thémés (`showConfirm`/`showPrompt`) pour la structure et la confirmation de perte, bandeau de conflit 409 non bloquant avec réessai, indicateur *dirty* (bouton + onglet) |
| 2026-09-29 | **Lot 3** livré : le Tableau de bord passe dans un **inspecteur droit repliable** (grille toujours visible), en-tête d'inspecteur avec entrée **Assistant IA** et fermeture, responsive < 900 px |
| 2026-09-29 | **Lot 4** livré : **undo/redo** (boutons + `Ctrl+Z`/`Ctrl+Maj+Z`/`Ctrl+Y`), chargement par `IntersectionObserver`, **ARIA** `role="grid"` ; le découpage modulaire est reporté en A5 |
+135 -10
View File
@@ -15,6 +15,7 @@ import { cacheViewedFile, getCachedFile } from './offline.js';
import { t } from './i18n.js';
import { onFileRender } from './plugins.js';
import { NowPlaying } from './now-playing.js';
import { openAssistant } from './ai-fab.js';
// ── Multi-format export ────────────────────────────────────────────────────
// Downloads a file export (HTML / MD bundle / ePub) via the authenticated
@@ -1112,6 +1113,19 @@ export function renderXlsxViewer(area, data) {
}
}
};
// #154-A4 — ARIA grid semantics (the backend renders a plain <table>).
const applyGridA11y = (panel) => {
const table = panel.querySelector(".xlsx-table");
if (!table) return;
table.setAttribute("role", "grid");
table.querySelectorAll("thead th").forEach((th) => th.setAttribute("role", "columnheader"));
table.querySelectorAll("tbody tr").forEach((tr) => {
tr.setAttribute("role", "row");
tr.querySelectorAll("th, td").forEach((cell) => {
cell.setAttribute("role", cell.tagName === "TH" ? "rowheader" : "gridcell");
});
});
};
const panels = isCsv
? `<div class="xlsx-panel" data-sheet="0">${sheets[0]?.html || data.html || ""}</div>`
: sheets.map((s, i) =>
@@ -1147,6 +1161,14 @@ export function renderXlsxViewer(area, data) {
// later into the dedicated (empty) “view” group.
const actionGroups = [];
if (!readOnly) {
actionGroups.push(`<span class="xlsx-cmd-group" data-group="history">
<button class="btn-action" id="xlsx-undo-btn" type="button" title="${escapeHtml(t("xlsx.undo"))}" disabled>
<i data-lucide="undo-2" style="width:14px;height:14px"></i>
</button>
<button class="btn-action" id="xlsx-redo-btn" type="button" title="${escapeHtml(t("xlsx.redo"))}" disabled>
<i data-lucide="redo-2" style="width:14px;height:14px"></i>
</button>
</span>`);
actionGroups.push(`<span class="xlsx-cmd-group" data-group="formulas">
<button class="btn-action xlsx-formula-toggle" id="xlsx-formula-btn" type="button"
aria-pressed="false" title="${escapeHtml(t("xlsx.formula_toggle_title"))}">f(x)</button>
@@ -1198,7 +1220,23 @@ export function renderXlsxViewer(area, data) {
</button>
</span>
</div>
<div class="xlsx-panels">${panels}</div>
<div class="xlsx-body">
<div class="xlsx-main">
<div class="xlsx-panels">${panels}</div>
</div>
<aside class="xlsx-inspector" id="xlsx-inspector" aria-hidden="true">
<div class="xlsx-inspector-head">
<span class="xlsx-inspector-title">${escapeHtml(t("xlsx.inspector_title"))}</span>
<span class="xlsx-inspector-actions">
<button class="btn-action xlsx-inspector-ai" id="xlsx-inspector-ai" type="button" title="${escapeHtml(t("xlsx.inspector_ai"))}">
<i data-lucide="bot" style="width:14px;height:14px"></i> ${escapeHtml(t("xlsx.inspector_ai_short"))}
</button>
<button class="btn-action xlsx-inspector-close" id="xlsx-inspector-close" type="button" aria-label="${escapeHtml(t("common.cancel"))}">✕</button>
</span>
</div>
<div class="xlsx-inspector-body" id="xlsx-inspector-body"></div>
</aside>
</div>
</div>`;
// ── #153 A17 — dashboard panel: named ranges, objects, per-sheet KPI ──
@@ -1212,14 +1250,26 @@ export function renderXlsxViewer(area, data) {
title: t("xlsx.dashboard_btn"),
}, [icon("layout-dashboard", 14), document.createTextNode(t("xlsx.dashboard_btn"))]);
(area.querySelector("#xlsx-view-group") || area.querySelector(".xlsx-toolbar-actions")).appendChild(dashBtn);
const inspector = area.querySelector("#xlsx-inspector");
const inspectorBody = area.querySelector("#xlsx-inspector-body");
const closeInspector = () => {
const panel = inspectorBody.querySelector(".xlsx-dashboard");
if (panel) panel.remove();
inspector.classList.remove("open");
inspector.setAttribute("aria-hidden", "true");
dashBtn.classList.remove("active");
};
dashBtn.addEventListener("click", async () => {
const old = area.querySelector(".xlsx-dashboard");
if (old) { old.remove(); dashBtn.classList.remove("active"); return; }
// #154-A3 — the dashboard lives in a right-hand inspector so the grid
// stays visible beside it (it used to be prepended into the grid flow).
if (inspectorBody.querySelector(".xlsx-dashboard")) { closeInspector(); return; }
dashBtn.classList.add("active");
inspector.classList.add("open");
inspector.setAttribute("aria-hidden", "false");
const panel = document.createElement("div");
panel.className = "xlsx-dashboard";
panel.innerHTML = `<div class="xlsx-dashboard-loading">…</div>`;
area.querySelector(".xlsx-panels").prepend(panel);
inspectorBody.prepend(panel);
try {
const dash = await api(
`/api/file/${encodeURIComponent(data.vault)}/xlsx/dashboard?path=${encodeURIComponent(data.path)}`,
@@ -1261,6 +1311,10 @@ export function renderXlsxViewer(area, data) {
panel.innerHTML = `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.load_error"))}: ${escapeHtml(String(err.message || err))}</p>`;
}
});
area.querySelector("#xlsx-inspector-close").addEventListener("click", closeInspector);
// #154-A3 — the assistant is the existing global side panel; the inspector
// just gives it a contextual entry point from the spreadsheet.
area.querySelector("#xlsx-inspector-ai").addEventListener("click", () => { openAssistant(); });
}
const saveBtn = area.querySelector("#xlsx-save-btn");
@@ -1359,6 +1413,7 @@ export function renderXlsxViewer(area, data) {
if (win.freeze && !sheetMeta.freeze) sheetMeta.freeze = win.freeze;
applySheetMeta(panel);
}
applyGridA11y(panel);
safeCreateIcons();
};
@@ -1382,11 +1437,20 @@ export function renderXlsxViewer(area, data) {
}
};
// Scroll sentinel: near the bottom of the wrapper, pull the next window.
wrapper.addEventListener("scroll", () => {
if (loading || done) return;
if (wrapper.scrollTop + wrapper.clientHeight >= wrapper.scrollHeight - 120) loadMore();
});
// #154-A4 — prefer IntersectionObserver (less main-thread work than a
// scroll listener); the scroll listener stays as a fallback for
// environments without IO (JSDOM).
if (typeof IntersectionObserver === "function") {
const io = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) loadMore();
}, { root: wrapper, rootMargin: "120px" });
io.observe(foot);
} else {
wrapper.addEventListener("scroll", () => {
if (loading || done) return;
if (wrapper.scrollTop + wrapper.clientHeight >= wrapper.scrollHeight - 120) loadMore();
});
}
};
// ── #153 A7 — keyboard navigation & formula bar ────────────────────────
@@ -1484,6 +1548,65 @@ export function renderXlsxViewer(area, data) {
}
});
// ── #154-A4 — local undo/redo for cell edits ──────────────────────────
// One command per committed edit (a cell loses focus with a new value).
// Structure changes re-render from the server, which rebuilds the whole
// viewer and therefore clears the stacks (they are per-render).
const undoStack = [];
const redoStack = [];
let pendingEdit = null;
const undoBtn = area.querySelector("#xlsx-undo-btn");
const redoBtn = area.querySelector("#xlsx-redo-btn");
const updateUndoButtons = () => {
if (undoBtn) undoBtn.disabled = undoStack.length === 0;
if (redoBtn) redoBtn.disabled = redoStack.length === 0;
};
const commitEdit = () => {
if (!pendingEdit) return;
const { td, before } = pendingEdit;
pendingEdit = null;
const after = td.textContent;
if (before === after) return;
undoStack.push({ sheet: td.closest(".xlsx-panel")?.dataset.sheet, ref: td.dataset.cell, before, after });
redoStack.length = 0;
updateUndoButtons();
};
const beginEdit = (td) => {
if (pendingEdit && pendingEdit.td !== td) commitEdit();
if (!pendingEdit) pendingEdit = { td, before: td.textContent };
};
const applyHistory = (entry, value) => {
const panel = area.querySelector(`.xlsx-panel[data-sheet="${entry.sheet}"]`) || panelEls[0];
const td = panel?.querySelector(`td[data-cell="${entry.ref}"]`);
if (!td) return;
td.textContent = value;
td.classList.toggle("xlsx-dirty", value !== (td.dataset.orig ?? ""));
if (td === activeTd) syncFormulaBar(td);
refreshSaveState();
};
const undo = () => {
const entry = undoStack.pop();
if (!entry) return;
applyHistory(entry, entry.before);
redoStack.push(entry);
updateUndoButtons();
};
const redo = () => {
const entry = redoStack.pop();
if (!entry) return;
applyHistory(entry, entry.after);
undoStack.push(entry);
updateUndoButtons();
};
if (undoBtn) undoBtn.addEventListener("click", undo);
if (redoBtn) redoBtn.addEventListener("click", redo);
area.querySelector(".xlsx-viewer").addEventListener("keydown", (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
const key = e.key.toLowerCase();
if (key === "z") { e.preventDefault(); if (e.shiftKey) redo(); else undo(); }
else if (key === "y") { e.preventDefault(); redo(); }
});
const setupCell = (td) => {
td.contentEditable = "true";
td.spellcheck = false;
@@ -1497,7 +1620,8 @@ export function renderXlsxViewer(area, data) {
syncFormulaBar(td);
refreshSaveState();
});
td.addEventListener("focus", () => setActiveCell(td));
td.addEventListener("focus", () => { beginEdit(td); setActiveCell(td); });
td.addEventListener("blur", commitEdit);
td.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); td.blur(); }
if (e.key === "Escape") {
@@ -1542,6 +1666,7 @@ export function renderXlsxViewer(area, data) {
}
panelEls.forEach(wireLazyRows);
panelEls.forEach(applySheetMeta);
panelEls.forEach(applyGridA11y);
// The formula bar starts disabled: nothing is selected yet.
setActiveCell(null);
+5
View File
@@ -1831,6 +1831,8 @@
"xlsx.conflict_msg": "The workbook was changed elsewhere in the meantime. Your edits are kept: retry the save.",
"xlsx.conflict_retry": "Retry",
"xlsx.formula_toggle_title": "Treat “=” and “@” as formulas (off by default)",
"xlsx.undo": "Undo the last change",
"xlsx.redo": "Redo the change",
"xlsx.cached_value_title": "Last value calculated by Excel",
"xlsx.tabs_add_sheet": "Add a sheet",
"xlsx.readonly_badge": "Read-only",
@@ -1877,6 +1879,9 @@
"xlsx.structure_error": "Could not change the structure",
"xlsx.last_sheet": "The last sheet cannot be deleted",
"xlsx.dashboard_btn": "Dashboard",
"xlsx.inspector_title": "Inspector",
"xlsx.inspector_ai": "AI assistant",
"xlsx.inspector_ai_short": "AI",
"xlsx.dashboard_title": "Workbook dashboard",
"xlsx.dashboard_stats": "{{cells}} cells · {{rows}} rows · {{cols}} columns · {{formulas}} formulas · {{numeric}} numeric values",
"xlsx.dashboard_charts": "{{n}} chart(s)",
+5
View File
@@ -1831,6 +1831,8 @@
"xlsx.conflict_msg": "Le classeur a été modifié ailleurs entre-temps. Vos modifications sont conservées : réessayez l'enregistrement.",
"xlsx.conflict_retry": "Réessayer",
"xlsx.formula_toggle_title": "Interpréter « = » et « @ » comme des formules (désactivé par défaut)",
"xlsx.undo": "Annuler la dernière modification",
"xlsx.redo": "Rétablir la modification",
"xlsx.cached_value_title": "Dernière valeur calculée par Excel",
"xlsx.tabs_add_sheet": "Ajouter une feuille",
"xlsx.readonly_badge": "Lecture seule",
@@ -1877,6 +1879,9 @@
"xlsx.structure_error": "Modification de la structure impossible",
"xlsx.last_sheet": "Impossible de supprimer la dernière feuille",
"xlsx.dashboard_btn": "Tableau de bord",
"xlsx.inspector_title": "Inspecteur",
"xlsx.inspector_ai": "Assistant IA",
"xlsx.inspector_ai_short": "IA",
"xlsx.dashboard_title": "Tableau de bord du classeur",
"xlsx.dashboard_stats": "{{cells}} cellules · {{rows}} lignes · {{cols}} colonnes · {{formulas}} formules · {{numeric}} valeurs numériques",
"xlsx.dashboard_charts": "{{n}} graphique(s)",
+65
View File
@@ -11289,6 +11289,71 @@ body.desktop-mode .editor-container {
color: var(--text-primary);
}
/* #154-A3 — right-hand inspector (dashboard, assistant) beside the grid. */
.xlsx-body {
display: flex;
align-items: stretch;
gap: 10px;
}
.xlsx-main {
flex: 1 1 auto;
min-width: 0;
}
.xlsx-inspector {
flex: 0 0 340px;
width: 340px;
max-width: 45%;
display: none;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
max-height: 70vh;
}
.xlsx-inspector.open {
display: flex;
}
.xlsx-inspector-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
}
.xlsx-inspector-title {
font-weight: 600;
font-size: 0.85rem;
color: var(--text-primary);
}
.xlsx-inspector-actions {
display: inline-flex;
align-items: center;
gap: 6px;
}
.xlsx-inspector-body {
flex: 1 1 auto;
overflow: auto;
padding: 10px;
}
.xlsx-inspector .xlsx-dashboard {
margin: 0;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
}
@media (max-width: 900px) {
.xlsx-body {
flex-direction: column;
}
.xlsx-inspector {
flex-basis: auto;
width: auto;
max-width: none;
}
}
/* #153 A17 — workbook dashboard panel: named ranges table + KPI cards.
Colors come from the existing CSS variables (no hardcoded values). */
.xlsx-dashboard {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.41.0",
"version": "2.43.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": {
+65
View File
@@ -719,6 +719,34 @@ await test("the dashboard button fetches the metadata and renders named ranges +
assert.equal(area.querySelector(".xlsx-dashboard"), null, "panel toggles closed");
});
await test("the dashboard opens in the right-hand inspector, not inline in the grid", async () => {
const area = mount();
apiQueue.push({
ok: true, status: 200,
body: { vault: "V", path: "data.xlsx", named_ranges: [], objects: { charts: 0, pivots: 0 }, sheets: [{ name: "Feuille1", cells: 1, rows: 1, cols: 1, formulas: 0, numeric: 0, kpi: [] }] },
});
area.querySelector("#xlsx-dashboard-btn").click();
await new Promise((r) => setTimeout(r, 5));
const inspector = area.querySelector("#xlsx-inspector");
assert.equal(inspector.classList.contains("open"), true, "the inspector opens");
assert.equal(inspector.getAttribute("aria-hidden"), "false");
assert.ok(inspector.querySelector("#xlsx-inspector-body .xlsx-dashboard"), "the dashboard lives in the inspector");
// The grid is still present beside it (not replaced).
assert.ok(area.querySelector(".xlsx-main .xlsx-table"), "the grid stays visible");
// Closing removes the panel and closes the inspector.
area.querySelector("#xlsx-inspector-close").click();
assert.equal(area.querySelector(".xlsx-dashboard"), null);
assert.equal(inspector.classList.contains("open"), false);
assert.equal(inspector.getAttribute("aria-hidden"), "true");
});
await test("the inspector exposes an AI assistant entry point", () => {
const area = mount();
assert.ok(area.querySelector("#xlsx-inspector-ai"), "AI button in the inspector head");
const title = area.querySelector("#xlsx-inspector .xlsx-inspector-title");
assert.equal(title.textContent, FR["xlsx.inspector_title"]);
});
await test("a csv mounts without the dashboard button", () => {
const area = document.getElementById("content-area");
area.innerHTML = "";
@@ -814,6 +842,7 @@ await test("a read-only workbook shows the read-only pill and offers no editing"
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("#xlsx-undo-btn"), null, "no history for read-only");
assert.equal(area.querySelector('td[data-cell="A1"]').getAttribute("contenteditable"), null, "cells are not editable");
});
@@ -842,6 +871,42 @@ await test("the command bar exposes grouped actions with a primary save", () =>
assert.ok(area.querySelector(".xlsx-cmd-group[data-group='file']"));
});
// ── #154-A4 — undo/redo, ARIA ───────────────────────────────────────────────
await test("undo/redo restores cell edits from the command stack", () => {
const area = mount();
const td = area.querySelector('td[data-cell="A1"]');
const undoBtn = area.querySelector("#xlsx-undo-btn");
const redoBtn = area.querySelector("#xlsx-redo-btn");
assert.equal(undoBtn.disabled, true, "nothing to undo yet");
td.focus();
editCell(area, "A1", "42");
td.dispatchEvent(new w.Event("blur"));
assert.equal(undoBtn.disabled, false, "undo becomes available after a committed edit");
undoBtn.click();
assert.equal(td.textContent, "100", "undo restores the original value");
assert.equal(area.querySelectorAll("td.xlsx-dirty").length, 0, "the cell is clean again");
assert.equal(redoBtn.disabled, false, "redo becomes available");
redoBtn.click();
assert.equal(td.textContent, "42", "redo reapplies the edit");
assert.equal(area.querySelectorAll("td.xlsx-dirty").length, 1, "the edit is dirty again");
});
await test("a no-op undo/redo pair leaves the buttons disabled", () => {
const area = mount();
assert.equal(area.querySelector("#xlsx-undo-btn").disabled, true);
assert.equal(area.querySelector("#xlsx-redo-btn").disabled, true);
});
await test("the grid exposes ARIA grid semantics", () => {
const area = mount();
const table = area.querySelector(".xlsx-table");
assert.equal(table.getAttribute("role"), "grid");
assert.equal(area.querySelector(".xlsx-table thead th").getAttribute("role"), "columnheader");
assert.equal(area.querySelector('td[data-cell="A1"]').getAttribute("role"), "gridcell");
assert.equal(area.querySelector("th.xlsx-rownum").getAttribute("role"), "rowheader");
});
// ── Report ──────────────────────────────────────────────────────────────────
console.log(`\n${passCount}/${testCount} tests passed\n`);
process.exit(passCount === testCount ? 0 : 1);