feat: éditeur tableur — grille A → Z par blocs, barre de menus Sheets, panneau de couleurs, création .xlsx (#180 → #186)
CI / lint (push) Successful in 2m43s
CI / security (push) Successful in 1m33s
CI / test (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s

Restaure et termine le lot tableur resté non committé. Il n'existait que dans un
`git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js,
color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le
stash, créé avant le commit BUG-108, n'a jamais été restauré.

#180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint),
grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu
Fichier au niveau des onglets, barre épinglée pleine largeur.

#181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans
quitter contenteditable, double-clic / F2 / première frappe qui remplace,
Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts.

#182 Désélection fiable (clic simple sans dépendre du focus), contours de plage
non empilés (sélecteur de classes sans point), couleurs texte/fond sur une
plage, clic droit qui préserve la sélection multiple.

#183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD,
PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était
rejetée par normHex).

#184 Peinture de format complète (toutes propriétés, source sans format =
réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille
étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au
défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules
câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de
cellules `ref → td`, court-circuits formule/styles, marqueur data-wired.

#185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36
annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème.

#186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC
(openpyxl) au lieu d'une charge utile texte illisible.

Corrections trouvées en restaurant et en exerçant le lot :
- fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque
  rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ;
- sorties Markdown / HTML / Imprimer de la barre de menus inertes
  (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ;
- collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu
  Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ;
- curseur col/row-resize absent quand le pointeur est sur la table elle-même ;
- l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown,
  HTML, impression) ;
- clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au
  seul appui long tactile) ;
- une entrée indisponible laissait la barre de menus ouverte.

Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ;
33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19,
xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer
19/19.
This commit is contained in:
2026-10-08 12:00:15 -04:00
parent ce1944d437
commit 634ba8a272
39 changed files with 9996 additions and 491 deletions
+297 -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.53.3**.
> [Unreleased](#unreleased). La dernière version livrée est **2.54.0**.
---
@@ -14,6 +14,302 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.54.0] — 2026-10-08
### Ajouté
- **#186 - Création d'un fichier Excel depuis « Créer un fichier »**
- **Excel (`.xlsx`) fait partie des types proposés** dans la modale « Créer un
fichier » (menu contextuel d'un vault ou d'un dossier, palette de commandes) :
le classeur s'ouvre directement dans la visionneuse, prêt à être saisi. Les 10
libellés du sélecteur passent en i18n FR/EN (`modal.ext_*`) — ils étaient
codés en dur en français.
- **Un vrai classeur, pas un fichier vide.** Un `.xlsx` est une archive OPC, pas
du texte : `create_file()` construit désormais l'archive avec openpyxl (une
feuille `Feuille1`) au lieu d'écrire la charge utile vide de la modale, qui
aurait produit un fichier que ni la visionneuse ni Excel n'ouvrent. Le champ
`content` est ignoré pour cette extension.
- **#183 — Panneau de sélection de couleurs façon Google Sheets (texte et fond)**
- Le ruban et le menu **Mise en forme** n'utilisent plus le sélecteur natif :
ils ouvrent un **panneau** conforme à la description fournie —
*Réinitialiser* en en-tête, palette **8 × 10** (ligne 1 = 8 nuances de gris
du noir au blanc, lignes 2 à 10 = 8 teintes × 9 nuances du clair au foncé),
section **STANDARD** (8 couleurs prédéfinies) avec crayon, section
**PERSONNALISÉ** (pastille + bouton « + » + crayon), pied de page
**Mise en forme conditionnelle** / **Couleurs en alternance**.
- Un **clic sur une pastille referme le panneau** et applique la couleur à la
cellule ou à la plage sélectionnée ; l'indicateur de sélection est une
coche **blanche ou noire selon la luminance** de la pastille.
- **Correction d'un piège d'encodage** : la palette est calculée en HSL pour
la lisibilité mais sort en **`#rrggbb`**. Une valeur `hsl(...)` était
silencieusement rejetée par `normHex()` — la couleur n'était donc jamais
écrite sur la cellule.
- Le chrome du panneau suit le **thème actif** (variables CSS uniquement) ;
« + » et les crayons ouvrent le sélecteur natif pour une couleur
arbitraire.
- Les deux entrées de pied de page sont **annoncées mais pas encore
disponibles** : elles affichent un message explicite plutôt qu'un bouton
inerte.
### Modifié
- **#185 - Tableur : barre de menus Google Sheets, boutons façon Sheets, grille unie**
- **Les dix menus de Google Sheets sont là** : Fichier, Modifier, Affichage,
Insertion, Format, Données, Outils, Gemini, Extensions, Aide — dans l'ordre
et avec les libellés exacts de Sheets (161 entrées). Le nouveau module
`frontend/js/xlsx/menus.js` décrit l'arborescence, `viewer.js` ne fait que
brancher chaque entrée sur un flux existant. Les sections imbriquées
(Nombre, Thème, Alignement, Zoom, Nettoyage…) sont des **accordéons**, une
seule ouverte à la fois, comme dans Sheets.
- **La majorité des fonctions sont opérationnelles** : Partager, Télécharger
(original / CSV / Markdown / HTML), Imprimer, **Renommer**, **Déplacer vers
la corbeille**, **Historique des versions**, Détails ; Annuler, Rétablir,
Couper, Copier, Coller, **Collage spécial (valeurs uniquement)**,
**Supprimer (lignes / colonnes / contenu)**, Tout sélectionner,
**Rechercher et remplacer** ; **Figer** (ici / ligne / colonne), **Afficher
→ barre de formule · grille · en-têtes · tableau de bord**, **Zoom**,
**Plein écran** ; Lignes / Colonnes / Cellules, Lien, Commentaire,
**Fonction** (curseur placé entre les parenthèses) ; Nombre, **Thème**,
Gras / Italique / Souligné / Barré, Taille de police, Alignement, Retour à la
ligne, Rotation, **Couleurs alternées**, Effacer la mise en forme ; Trier,
**Créer un filtre**, **Plages nommées**, **Nettoyage des données
(doublons / espaces superflus)**, **Diviser le texte en colonnes** ;
**Saisie semi-automatique**, **Accessibilité** ; Gemini (6 entrées vers
l'assistant IA) ; Aide, **Mises à jour**, **Raccourcis clavier**,
**Signaler un problème** (diagnostic copié dans le presse-papiers).
- **Les options qu'ObsiGate n'a pas le disent** : les 36 entrées absentes
(macros, Apps Script, tableaux croisés, graphiques, formulaires, modules,
AppSheet, Looker Studio, formation…) restent **listées** pour tenir la
parité demandée, mais un clic y répond « *« X » n'est pas disponible dans
ObsiGate* » au lieu de ne rien faire.
- **Le menu Fichier a quitté la barre d'onglets** pour la barre de menus, en
**conservant ses identifiants** (`#xlsx-file-btn`, `#xlsx-file-menu`,
`#xlsx-download-btn`, `#xlsx-csv-btn`) : seuls les raccourcis internes et les
tests bougent. Seul **Enregistrer** reste épinglé à droite des onglets.
- **Ruban refait façon Google Sheets** : boutons plats de 30 × 30, sans bordure
ni fond au repos, survol discret, état actif teinté, groupes séparés par des
filets verticaux. **Enregistrer** reste le seul pilier plein. Nouveau bouton
**grille**, synchronisé avec Affichage ▸ Afficher ▸ Grille.
- **Grille unie et visible** : chaque cellule est encadrée sur **les quatre
côtés** par **la même** ligne de 1 px, en-têtes compris (ils se distinguent
par leur fond, plus par une bordure plus lourde). `--grid-border` n'est plus
`--border`, quasi identique au fond des cellules sur la plupart des thèmes :
c'est désormais un `color-mix` du texte vers la surface, calculé par
`themes.js`, donc une seule valeur pour toute la grille et lisible sur les 15
thèmes comme sur les modes contraste élevé et sépia.
- **#184 - Tableur : peinture de format complète, sélection de lignes/colonnes
multiples, grille A → Z × 1000 lignes**
- **Peindre le format ne ratait plus aucun cas.** L'outil copiait un
sous-ensemble de propriétés et **refusait** une cellule source sans mise en
forme explicite — donc une cellule simple vers une cellule colorée et en
grande taille ne produisait rien. La copie porte maintenant sur **toute** la
mise en forme (gras, italique, souligné, barré, couleur du texte, couleur de
fond, taille, alignement, valign, retour ligne, rotation, format de nombre,
bordures) ; une source **sans** format **réinitialise** la cible au lieu de
ne rien faire.
- **Fuite corrigée** : annuler la peinture (`Échap`, second clic) laissait le
gestionnaire « appliquer au prochain clic » en place — le clic suivant dans la
grille repeignait une vieille mise en forme. Il est maintenant désinstallé.
- **Sélection de plusieurs lignes / colonnes depuis les marges**, comme dans
Sheets et Excel : glisser sur les numéros de ligne (ou `Maj`+clic) sélectionne
une plage de lignes entières ; idem sur les lettres de colonne. Le
redimensionnement par les bords d'en-tête reste prioritaire sur ce geste.
- **Grille étendue à A → Z, et jusqu'à 1000 lignes**, les cellules vides
étant de **vraies cellules éditables**. Les colonnes sont posées d'emblée ;
les lignes s'ajoutent **par blocs de 100 au défilement**, et non d'un seul
coup : matérialiser 1000 lignes revient à ~26 000 cellules câblées par
feuille, ce qui épuisait le tas de la suite JSDOM. Une feuille tronquée
garde son chargement serveur « Charger la suite » (les lignes manquantes
sont des données, pas du remplissage).
- **Trois goulots d'étranglement levés**, sinon la grille étendue rendait
l'éditeur inutilisable : `findTd` faisait un `querySelector` par appel
(sélectionner une colonne = 26 000 requêtes) → **index de cellules** avec
auto-réparation ; le rafraîchissement des formules clonait 26 000 nœuds à
chaque frappe → **court-circuit** sur les cellules vides ; l'application des
styles touchait 26 000 cellules → **court-circuit** sur celles que le
classeur ne style pas.
- Marqueur `data-wired` explicite : `data-orig` valait `""` pour une cellule
vide, ce qui recâblait les cellules déjà câblées (double écouteur).
- **#181 — Éditeur tableur : modèle de saisie Google Sheets (sélection ≠ édition)**
- **Deux états distincts.** Une cellule est *sélectionnée* (clic, flèches) ou
*en cours d'édition*, et plus les deux en même temps. Le curseur de texte
n'apparaît plus en sélection (`caret-color: transparent`, sans quitter
`contenteditable`) : on ne voit plus un curseur clignotant au clic ou aux
flèches.
- **Entrées en édition**, comme Google Sheets / Excel : **double-clic** (le
curseur est placé *sous le pointeur*, l'édition se fait en place), **première
frappe** — qui **remplace** le contenu au lieu de s'y ajouter —, ou `F2`.
`Alt`+flèche insère un saut de ligne, `Échap` quitte l'édition et restaure
la valeur.
- **`Entrée` contextuelle** : en édition, elle valide et déplace le focus sur
**la cellule du dessous** (au lieu de perdre le focus) ; en sélection, elle
bascule la cellule courante en mode édition. `Tab` suit la même logique.
- **Indicateur de sélection renforcé** : la plage est **contourée** comme dans
Excel / Sheets (quatre classes de bord sur les côtés réels du bloc, pas un
simple lavis), et les en-têtes couverts — lettres `A`, `B`… et numéros
`1`, `2`… — sont reflétés.
- Un clic simple après une édition abandonne l'état d'édition : plus de
curseur résiduel.
- **Inventaire des écarts** vs Google Sheets / Excel dressé et priorisé
(poignée de recopie, *remplacer*, validation de données, filtre
automatique, tri multi-critères, mise en forme conditionnelle, création de
graphiques, protection…) : [fiche #181](docs/features/xlsx-sheet-input-181.md).
- Tests JSDOM : `xlsx-viewer` passe de 136 à **146/146** (10 cas #181).
- **#180 — Éditeur tableur : redimensionnement au curseur, grille thémée, menu
Fichier & barre épinglée**
- **Curseur de redimensionnement.** Survoler le bord d'un en-tête de colonne
(`A`, `B`, `C`…) ou de ligne (`1`, `2`, `3`…) affiche désormais l'icône
`col-resize` / `row-resize`. Le curseur par défaut des cellules
(`cell`) et des en-têtes (`pointer`) masquait auparavant l'indicateur, y
compris pendant le glissement : les en-têtes concerned sont désormais
miroirés en ligne et la table porte une classe de repaint
(`.xlsx-resize-col` / `.xlsx-resize-row`).
- **Grille compatible tous les thèmes.** Les jetons de grille
(`--grid-*`) sont dérivés du thème actif (`--surface`, `--bg-hover`,
`--text-primary`, `--border`, bandeau zébré en `color-mix`) : les
15 thèmes × 4 modes de l'application, y compris les modes générés
contrasté élevé / sépia et les thèmes importés, sont désormais
respectés, plus les couleurs figées du thème par défaut.
- **Menu Fichier.** Télécharger, CSV, Markdown, HTML et Imprimer sont
réunis dans un seul menu **Fichier**, placé à droite de la barre des
onglets de feuilles, avec le bouton **Enregistrer** juste à côté. Le
ruban conserve les commandes d'édition. Les identifiants des contrôles
(`#xlsx-download-btn`, `#xlsx-csv-btn`, `#xlsx-save-btn`) sont
inchangés.
- **Barre épinglée pleine largeur.** La barre de boutons est épinglée en
haut, au bord de la marge de la page, et l'éditeur occupe toute la
largeur du contenu (comme les visionneuses PDF / images / Excalidraw).
- **Indicateur retiré.** La pastille « Formules calculées localement »
disparaît : le calcul local est le comportement normal, pas une limite
worthy of a badge.
### Corrigé
- **#182 — Éditeur tableur : les encadrés de la sélection s'empilaient et ne
suivaient plus le focus**
- **Cause racine.** Le sélecteur CSS de nettoyage des contours de plage était
écrit **sans le point** (`querySelectorAll("xlsx-sel-top,…")`). En CSS, un
nom nu désigne une **balise** (`<xlsx-sel-top>`), pas une classe : la requête
ne renvoyait **rien** et aucun contour n'était jamais effacé. Les encadrés
restaient donc en place et s'accumulaient à chaque changement de cellule.
Même famille de faute sur les en-têtes reflétés (`classList.remove` appelé
avec un point).
- **Comportement corrigé** : le contour suit désormais exactement la sélection —
une cellule seule est encadrée, une plage est encadrée sur ses quatre bords
réels, et **rien ne reste** sur les cellules quittées (navigation clavier,
clic, changement de cellule, changement de feuille).
- **Écart Sheets corrigé au passage** : pendant la saisie, les flèches
**valident et déplacent** la sélection (auparavant elles déplaçaient le
curseur dans le texte et la cellule semblait figée). `Alt`+flèche conserve
l'insertion d'un saut de ligne.
- Non-régression : `xlsx-viewer` passe de 149 à **152/152** (3 cas #182).
- **#182 — Éditeur tableur : la sélection multiple ne se désélectionnait plus, et
les couleurs paraissaient inertes sur une plage**
- **Cause racine de la sélection « collée ».** La désélection reposait sur
l'événement `focus` de la cellule. Or `Maj`+clic appelle `preventDefault()`,
ce qui **empêche le navigateur de déplacer le focus** : `activeTd` et le
focus réel divergeaient. Un clic suivant sur la cellule déjà focalisée ne
déclenchait donc **aucun** `focus` et la plage restait affichée. Le clic
simple efface désormais la plage explicitement dans `mousedown`, sans
dépendre du focus ; après un `Maj`+clic le focus suit enfin la cellule
cliquée (sous `suppressSelectionReset`, pour ne pas écraser la plage).
- **Zone Nom** : après un glissement, elle affiche de nouveau la **plage**
(`A1:B2`) au lieu de la seule cellule d'origine (`A1`).
- **Couleurs sur une plage** : vérifié sur les trois chemins (menu Mise en
forme, nuanciers du ruban, sélection par `Maj`+flèches ou par glissement) —
la charge utile porte bien la plage (`A1:B2`) et les 4 cellules reçoivent
`color` **et** `background`.
- **Retour visuel manquant** : les deux nuanciers du ruban affichent désormais
une **pastille** qui prend la couleur choisie, ce qui donnait l'impression
que la couleur n'était pas appliquée.
- Non-régression : `xlsx-viewer` passe de 146 à **149/149** (3 cas #182).
- **Visionneuse tableur / CSV : plus de ligne teintée au survol, la teinte de
fond est réservée aux sélections de 2 cellules et plus (Google Sheets)**
- Le survol ne colore **plus rien** : les règles `tr:hover` de la grille
XLSX (ligne entière en `--bg-hover`, y compris sur une cellule déjà
sélectionnée) et du tableau CSV sont retirées.
- Un clic simple ne change **plus le fond de la cellule** : elle ne garde que
son cadre accent (les quatre bords `xlsx-sel-*` + le contour
`xlsx-active`), comme dans Google Sheets.
- La teinte `.xlsx-selected` est désormais conditionnée par la classe
`.xlsx-range-multi`, posée/retirée sur `.xlsx-viewer` par
`applySelection()` selon l'étendue de la sélection — elle n'apparaît donc
qu'à partir de deux cellules. Les en-têtes de ligne / de colonne couverts
restent reflétés, comme dans Sheets.
- **Visionneuse tableur : le clic droit écrasait la sélection multiple avant
l'ouverture du menu contextuel**
- **Cause.** Le `mousedown` du bouton droit laissait le navigateur déplacer
le focus sur la cellule ; le handler `focus` appelait `setActiveCell()` et
réduisait la plage à cette cellule **avant** que le menu ne s'ouvre. Les
entrées Copier / Coller / Effacer / Insérer / Supprimer ne s'appliquaient
donc plus qu'à une cellule (`Delete 1 row(s)` au lieu de
`Delete 2 row(s)`), et la plage disparaissait à l'écran.
- **Correctif.** `preventDefault()` sur le `mousedown` de bouton droit dans
la grille : le focus ne bouge plus, la sélection multiple survit jusqu'au
choix d'une option. Un clic droit **hors** plage déplace toujours la
sélection sur la cellule visée (comportement Sheets), via
`selectForTarget()` ; le clic gauche, lui, écrase toujours la plage (#182).
- Vérifié en conditions réelles sur l'instance Docker : la plage `A2:C3` est
conservée à l'ouverture du menu, le menu annonce 2 lignes / 3 colonnes, et
Copier place bien les 2 lignes × 3 colonnes dans le presse-papiers.
- **#180 — 8 tests JSDOM réalignés** sur la sémantique « écriture verrouillée
puis application locale » des flux structure / mise en forme (le viewer
n'est plus re-lu après chaque écriture) : le second appel PUT est absent,
les assertions portent sur la grille peinte en place.
- **Tableur — les sorties du menu Fichier étaient inertes.** Les entrées
Markdown, HTML et Imprimer portaient `data-xlsx-export` mais aucun
`data-xlsx-action`, et le répartiteur générique ne lit que ce dernier : le
clic ne produisait rien (les identifiants `#xlsx-download-btn` et
`#xlsx-csv-btn`, eux, fonctionnaient). Elles sont câblées sur
`export-md` / `export-html` / `print`, donc sur les fonctions déjà en place.
- **Tableur — collision de classe sur `.xlsx-structure-menu`.** Les menus
déroulants de la nouvelle barre de menus portaient la même classe que le
menu Structure de la barre d'outils : toute requête `.xlsx-structure-menu`
tombait sur le premier nœud rencontré, donc sur un élément masqué. Les menus
de la barre prennent `.xlsx-menudrop` ; l'aspect reste partagé par le CSS.
- **Tableur — curseur de redimensionnement absent sur la table.** La sonde
géométrique était conditionnée à la présence d'un en-tête sous le pointeur ;
sur la table elle-même aucun en-tête n'est nommé et le curseur ne
s'affichait pas. La table retombe désormais sur la sonde géométrique.
- **Tableur — l'export emportait le quadrillage vide.** `sheetMatrix()`
renvoyait la grille telle que mise en page : avec les colonnes étendues à
A → Z, un classeur de deux colonnes exportait 26 colonnes de vide (CSV,
Markdown, HTML, impression). Les lignes et colonnes vides de queue sont
écartées — l'export porte le bloc utilisé, comme l'export d'un tableur.
- **Tableur — fuite mémoire à chaque rendu.** `renderXlsxViewer()` ajoutait un
écouteur `click` **anonyme** sur `#content-area` : l'élément survit au
re-rendu et la fermeture de l'écouteur retenait la grille précédente en
entier. Ouvrir ou enregistrer un classeur plusieurs fois faisait croître le
tas sans borne. Gestionnaire nommé, démonté en tête de rendu.
- **Tableur — le clic extérieur laissait le menu contextuel ouvert.** La fenêtre
de grâce de 250 ms, destinée au `click` résiduel d'un appui long tactile,
s'appliquait aussi au clic droit souris — qui n'en émet aucun : un clic
délibéré fait juste après l'ouverture était avalé par la grâce. Elle ne
couvre plus que l'ouverture tactile.
- **Tableur — une entrée indisponible ne refermait pas la barre de menus.** Le
répartiteur annonçait « *X* n'est pas disponible dans ObsiGate » puis sortait
avant la fermeture : le menu restait ouvert. Tout choix referme désormais le
menu, y compris une entrée qui ne fait que l'annoncer.
---
## [2.53.3] — 2026-10-07
### Corrigé