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
+732 -52
View File
@@ -62,7 +62,7 @@
--grid-bg: #161b22;
--grid-header-bg: #1f2430;
--grid-header-text: #e6edf3;
--grid-border: #30363d;
--grid-border: #3d444d;
--grid-zebra: rgba(255, 255, 255, 0.025);
}
@@ -111,7 +111,7 @@
--grid-bg: #161b22;
--grid-header-bg: #1f2430;
--grid-header-text: #e6edf3;
--grid-border: #30363d;
--grid-border: #3d444d;
--grid-zebra: rgba(255, 255, 255, 0.025);
}
@@ -160,10 +160,28 @@
--grid-bg: #ffffff;
--grid-header-bg: #eaeef2;
--grid-header-text: #1f2328;
--grid-border: #d0d7de;
--grid-border: #b6bec9;
--grid-zebra: rgba(0, 0, 0, 0.025);
}
/* ===== THEME — HIGH-CONTRAST (generated mode, see themes.js) ===== */
:root[data-theme="high-contrast"] {
--grid-bg: #000000;
--grid-header-bg: #141414;
--grid-header-text: #ffffff;
--grid-border: #6b6b6b;
--grid-zebra: rgba(255, 255, 255, 0.045);
}
/* ===== THEME — SEPIA (generated mode, see themes.js) ===== */
:root[data-theme="sepia"] {
--grid-bg: #f5ecd5;
--grid-header-bg: #e8dcc0;
--grid-header-text: #3d2e1a;
--grid-border: #b09a6d;
--grid-zebra: rgba(61, 46, 26, 0.04);
}
/* ===== BASE ===== */
html {
font-size: 16px;
@@ -1491,10 +1509,12 @@ select {
max-width: 1200px;
}
/* Full-bleed viewers (PDF, images, Excalidraw) must use the whole width when
the navigation sidebar is hidden instead of the centered reading column. */
/* Full-bleed viewers (PDF, images, Excalidraw, spreadsheets) must use the
whole width when the navigation sidebar is hidden instead of the centered
reading column. */
.sidebar.hidden ~ .content-wrapper .content-area:has(.pdf-viewer-container),
.sidebar.hidden ~ .content-wrapper .content-area:has(.image-viewer-container),
.sidebar.hidden ~ .content-wrapper .content-area:has(.xlsx-viewer),
.sidebar.hidden ~ .content-wrapper .content-area:has(iframe[src*="excalidraw-editor.html"]) {
margin: 0;
max-width: none;
@@ -10998,24 +11018,33 @@ body.desktop-mode .editor-container {
padding: 6px 12px;
border-bottom: 1px solid var(--border-light, var(--border));
}
.csv-table tr:hover td {
background: var(--surface);
}
/* Google Sheets : le survol d'une ligne ne teinte rien (jamais de highlight
de ligne entière au survol). */
/* ── 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). */
/* #154-A1 — the viewer shell is a grouped command bar (edition commands),
then a sheet strip (tabs on the left, File menu + Save on the right), then
a status row stating the viewer's limits (read-only). #180 pins the bar to
the top edge of the page, full content width. */
.xlsx-toolbar {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
padding: 8px 10px;
position: sticky; /* #180 — stays on the top edge while the grid scrolls */
/* La marge haute négative (pleine largeur) tirait la barre 40 px sous sa
position statique : `top: 0` la replaquait et elle recouvrait le contenu
(en-tête A–Z, ligne 1) de 32 px. La contrainte reprend la même expression
que la marge : aucun déplacement au repos, épinglage intact au scroll. */
top: calc(-1 * clamp(16px, 3vw, 40px));
z-index: 30;
margin: calc(-1 * clamp(16px, 3vw, 40px)) calc(-1 * clamp(16px, 4vw, 40px)) 8px;
padding: 8px clamp(16px, 4vw, 40px);
border: 1px solid var(--border);
border-radius: var(--radius);
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
background: var(--surface2);
position: relative; /* anchors the A14 structure menu */
}
.xlsx-cmdbar {
display: flex;
@@ -11023,6 +11052,277 @@ body.desktop-mode .editor-container {
gap: 10px;
flex-wrap: wrap;
}
/* #185 — the Google Sheets menu bar: a flat row of text buttons above the
ribbon. No chrome, no borders: only the open menu gets a filled pill. */
.xlsx-menubar {
display: flex;
align-items: center;
gap: 2px;
flex-wrap: wrap;
margin: -2px 0 -2px;
}
/* No `position: relative` here on purpose: the dropdown is positioned by
anchorUnder() (viewer.js) relative to the sticky `.xlsx-toolbar`, exactly like
the Structure / Mise en forme / ribbon menus. Making the wrapper a containing
block would shift every dropdown by the button's offset. */
.xlsx-menuwrap {
display: inline-flex;
}
.xlsx-menubtn {
appearance: none;
border: none;
background: transparent;
color: var(--text-secondary);
font-family: inherit;
font-size: 0.82rem;
line-height: 1.4;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.xlsx-menubtn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.xlsx-menubtn[aria-expanded="true"] {
background: var(--accent-bg, var(--bg-hover));
color: var(--accent-text, var(--accent, #4a90d9));
}
/* The Gemini entry keeps its own accent so it reads as the AI corner of the
bar, like the themed icon in Google Sheets. */
.xlsx-menuwrap-gemini .xlsx-menubtn {
color: var(--accent, #4a90d9);
font-weight: 600;
}
.xlsx-menudrop {
min-width: 260px;
max-height: min(70vh, 560px);
overflow-y: auto;
z-index: 40;
}
/* Accordion section (Nombre, Thème, Zoom, Alignement, …). */
.xlsx-menu-sub {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 18px;
border-left: 2px solid var(--border);
margin: 2px 0 4px 8px;
}
.xlsx-menu-sub[hidden] {
display: none;
}
.xlsx-menu-item {
width: 100%;
}
.xlsx-menu-item-d2 {
padding-left: 18px;
}
.xlsx-menu-caret {
width: 14px;
height: 14px;
flex: 0 0 auto;
margin-left: auto;
color: var(--text-muted);
transition: transform 0.12s ease;
}
.xlsx-menu-item[aria-expanded="true"] > .xlsx-menu-caret {
transform: rotate(90deg);
}
/* An entry ObsiGate does not implement: dimmed but still clickable, so it
answers instead of pretending. */
.xlsx-menu-item-na {
opacity: 0.55;
}
.xlsx-menu-item-na:hover {
opacity: 1;
}
.xlsx-menu-empty {
padding: 6px 10px;
font-size: 0.78rem;
color: var(--text-muted);
}
/* #185 — Google Sheets' ribbon look: flat square icon buttons, no border and
no resting background, grouped by thin vertical rules. The old rounded /
bordered chrome fought with the theme of every one of the 15 themes. */
.xlsx-toolbar .btn-action {
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--text-secondary);
min-width: 30px;
height: 30px;
padding: 0 6px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.xlsx-toolbar .btn-action:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--text-primary);
}
.xlsx-toolbar .btn-action.active {
background: var(--accent-bg, var(--bg-hover));
color: var(--accent-text, var(--accent, #4a90d9));
}
.xlsx-toolbar .btn-action:disabled {
opacity: 0.4;
cursor: default;
}
.xlsx-toolbar .xlsx-cmd-sep {
width: 1px;
height: 22px;
background: var(--border);
}
.xlsx-toolbar .xlsx-select {
height: 30px;
border-color: transparent;
background: transparent;
}
.xlsx-toolbar .xlsx-select:hover {
background: var(--bg-hover);
}
/* Save stays the one solid pill: it is the only command with a consequence
outside the browser. */
.xlsx-toolbar .xlsx-save-primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
font-size: 0.8rem;
}
.xlsx-toolbar .xlsx-save-primary:hover:not(:disabled) {
background: var(--accent);
color: #fff;
filter: brightness(1.08);
}
.xlsx-formula-bar[hidden] {
display: none;
}
/* Wrapped command groups: every control stays visible, no scrollbar. */
.xlsx-cmdbar > .xlsx-toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
flex-wrap: wrap;
overflow: visible;
}
.xlsx-cmdbar .xlsx-cmd-group,
.xlsx-cmdbar .xlsx-cmd-sep {
flex: 0 0 auto;
}
/* Native selects (zoom, number format): no custom dropdown to dismiss. */
.xlsx-select {
font-family: inherit;
font-size: 0.75rem;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
cursor: pointer;
max-width: 130px;
}
.xlsx-select:hover {
color: var(--accent);
border-color: var(--accent);
}
/* B/I/S/U letter buttons + decimal shorthands. */
.xlsx-fontstyle {
font-family: Georgia, "Times New Roman", serif;
min-width: 28px;
}
.xlsx-dec {
font-family: "JetBrains Mono", monospace;
}
.xlsx-fontsize {
min-width: 26px;
text-align: center;
font-size: 0.75rem;
color: var(--text-secondary);
}
/* Colour pickers: a letter/swatch label wrapping the native input. */
.xlsx-colorpick {
position: relative;
overflow: hidden;
}
.xlsx-colorpick input[type="color"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
padding: 0;
border: none;
}
/* #182 — colour swatch under the picker: mirrors the chosen colour so applying
one to a selection is visible on the button itself. Empty (transparent)
until a colour is actually picked, so the default ribbon stays clean. */
.xlsx-color-swatch {
width: 14px;
height: 4px;
border-radius: 2px;
border: 1px solid var(--border);
background: transparent;
flex: 0 0 auto;
}
.xlsx-colorletter {
font-family: Georgia, serif;
font-weight: 700;
border-bottom: 3px solid var(--accent);
line-height: 1;
padding: 0 2px;
}
/* Ribbon dropdown menus share the structure-menu look. */
.xlsx-ribbon-menu {
min-width: 220px;
}
/* Floating contextual mini-bar (multi-cell selections). */
.xlsx-minibar {
position: fixed;
z-index: 10060;
display: flex;
align-items: center;
gap: 2px;
padding: 4px 6px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
box-shadow: 0 8px 24px var(--shadow, rgba(0, 0, 0, 0.25));
}
.xlsx-minibar[hidden] {
display: none;
}
.xlsx-minibar button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
}
.xlsx-minibar button:hover {
background: var(--bg-hover);
color: var(--accent);
}
.xlsx-minibar button svg,
.xlsx-minibar button i[data-lucide] {
width: 15px;
height: 15px;
}
@media (pointer: coarse) {
.xlsx-minibar button {
width: 44px;
height: 44px;
}
}
.xlsx-toolbar-actions {
margin-left: auto;
display: flex;
@@ -11082,6 +11382,31 @@ body.desktop-mode .editor-container {
border-color: var(--accent);
color: var(--accent);
}
/* #180 — sheet strip: tabs on the left, File menu + Save pinned right. */
.xlsx-sheetbar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.xlsx-sheetbar .xlsx-tabs {
flex: 1 1 auto;
min-width: 0;
}
.xlsx-sheetbar-end {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
flex: 0 0 auto;
}
.xlsx-file-dropdown {
position: relative;
display: inline-block;
}
.xlsx-file-item {
text-align: left;
}
.xlsx-status-bar {
display: flex;
align-items: center;
@@ -11162,28 +11487,45 @@ body.desktop-mode .editor-container {
white-space: pre-line;
}
.xlsx-table {
background: var(--grid-bg);
background: var(--grid-bg, var(--surface));
}
/* #185 — one uniform 1px grid line on EVERY side of every cell (header rows
included), the way Sheets draws it. Previously only the bottom edge existed
and its colour came from `--border`, which is far too close to the cell
background on most themes: the grid read as a smear instead of a grid.
`--grid-border` is now derived from the theme by themes.js (same token for
every line, so the mesh stays perfectly regular). */
.xlsx-table td,
.xlsx-table th {
border: 1px solid var(--grid-border, var(--border));
}
.xlsx-table td {
border-bottom: 1px solid var(--grid-border);
border-bottom: 1px solid var(--grid-border, var(--border));
/* Cellules de données : une seule et même couleur (fond de grille). Pas de
variante une-ligne-sur-deux ici : sa spécificité écraserait la teinte de
sélection (une ligne sur deux non teintée). */
background: var(--grid-bg, var(--surface));
}
/* #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);
/* Cellules fusionnées : couleur d'en-tête, sauf en sélection (la teinte de
sélection gagne, uniforme sur toutes les cellules). */
.xlsx-table tbody td[rowspan]:not([rowspan="1"]):not(.xlsx-selected),
.xlsx-table tbody td[colspan]:not([colspan="1"]):not(.xlsx-selected) {
background: var(--grid-header-bg, var(--bg-hover));
color: var(--grid-header-text, var(--text-primary));
}
/* Google Sheets : aucun survol teinté — ni la ligne entière, ni la cellule.
Rien ne se met en couleur tant que l'utilisateur n'a pas sélectionné au
moins 2 cellules (voir `.xlsx-range-multi`). */
.xlsx-table th.xlsx-corner,
.xlsx-table th.xlsx-rownum {
background: var(--grid-header-bg);
color: var(--grid-header-text);
background: var(--grid-header-bg, var(--bg-hover));
color: var(--grid-header-text, var(--text-primary));
font-weight: 500;
text-align: right;
padding: 6px 8px;
border-bottom: 2px solid var(--grid-border);
border-right: 1px solid var(--grid-border);
/* #185 — the same 1px line as the cells: the header is told apart by its
fill, not by a heavier rule that broke the grid's regularity. */
border: 1px solid var(--grid-border, var(--border));
position: sticky;
left: 0;
/* #153 A8 — `top: auto` is load-bearing: `.csv-table th` pins EVERY `th`
@@ -11194,7 +11536,9 @@ body.desktop-mode .editor-container {
}
.xlsx-table th.xlsx-corner {
left: 0;
top: 0;
/* Suit `--xlsx-stick` comme l'en-tête : mesuré en JS sur le recouvrement
réel de la toolbar sticky, sinon la ligne A–Z glisse dessous. */
top: var(--xlsx-stick, 0px);
z-index: 4;
}
/* #153 A8 — the column headers stay visible while the sheet scrolls down.
@@ -11202,11 +11546,43 @@ body.desktop-mode .editor-container {
order is intentional: thead (3) < row numbers (2) < corner (4). */
.xlsx-table thead th {
position: sticky;
top: 0;
top: var(--xlsx-stick, 0px);
z-index: 3;
background: var(--grid-header-bg);
color: var(--grid-header-text);
border-bottom: 2px solid var(--grid-border);
background: var(--grid-header-bg, var(--bg-hover));
color: var(--grid-header-text, var(--text-primary));
border-bottom: 1px solid var(--grid-border, var(--border));
}
/* #185 — Affichage ▸ Afficher ▸ Grille / En-têtes, and Outils ▸ Accessibilité:
display concerns only, never a change to the file. */
.xlsx-viewer.xlsx-no-grid .xlsx-table td,
.xlsx-viewer.xlsx-no-grid .xlsx-table th {
border-width: 0;
}
.xlsx-viewer.xlsx-no-headers thead,
.xlsx-viewer.xlsx-no-headers th.xlsx-rownum,
.xlsx-viewer.xlsx-no-headers th.xlsx-corner {
display: none;
}
.xlsx-viewer.xlsx-a11y .xlsx-table td {
padding: 8px 10px;
font-size: 0.95rem;
}
.xlsx-viewer.xlsx-a11y {
--grid-border: color-mix(in srgb, var(--text-primary, #888) 42%, var(--surface, #000));
}
/* #180 — while the pointer rides a header edge, the representative resize
icon must win over the cell (`cell`) and header (`pointer`) cursors. The
edge header itself is mirrored inline by the viewer (its inline
`cursor:pointer` beats any stylesheet rule). */
.xlsx-table.xlsx-resize-col,
.xlsx-table.xlsx-resize-col th,
.xlsx-table.xlsx-resize-col td {
cursor: col-resize;
}
.xlsx-table.xlsx-resize-row,
.xlsx-table.xlsx-resize-row th,
.xlsx-table.xlsx-resize-row td {
cursor: row-resize;
}
.xlsx-table td[contenteditable] {
/* #155 — Excel-style cross cursor over the cells. */
@@ -11218,6 +11594,57 @@ body.desktop-mode .editor-container {
outline: 2px solid var(--accent, #4a90d9);
outline-offset: -2px;
}
/* #181-A1 — SELECTION vs EDITION, like Google Sheets / Excel.
A selected cell is outlined but must NOT show a text caret; the caret is
only revealed in edit mode (double-click, F2 or the first keystroke).
`caret-color: transparent` hides it without leaving `contenteditable`, so the
click-to-select / type-to-replace flow stays native. */
.xlsx-table td.xlsx-cell {
caret-color: transparent;
}
.xlsx-table td.xlsx-editing {
caret-color: auto;
cursor: text;
}
/* The editing cell also reads as "open": a solid accent frame plus a raised
shadow, so the state is visible even on a row that is part of a range. */
.xlsx-table td.xlsx-editing {
outline: 2px solid var(--accent, #4a90d9);
outline-offset: -2px;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
z-index: 1;
position: relative;
}
/* #181-A2 — range indicator: the four edges of the selected block trace it
exactly (Excel/Sheets draw a rectangle, not a flat wash), and the covered
row/column headers are highlighted too. `outline` on inner edges would be
overdrawn by the next cell, so the four borders are declared separately and
win over `.xlsx-selected`'s background. */
.xlsx-table tbody td.xlsx-sel-top {
border-top: 2px solid var(--accent, #4a90d9);
}
.xlsx-table tbody td.xlsx-sel-bottom {
border-bottom: 2px solid var(--accent, #4a90d9);
}
.xlsx-table tbody td.xlsx-sel-left {
border-left: 2px solid var(--accent, #4a90d9);
}
.xlsx-table tbody td.xlsx-sel-right {
border-right: 2px solid var(--accent, #4a90d9);
}
/* A one-cell selection keeps its four accent edges (a framed box) but NO fill:
`.xlsx-range-multi` is toggled on the viewer by applySelection() and gates
the tint, which starts at 2 cells — Google Sheets behaviour. */
.xlsx-viewer.xlsx-range-multi .xlsx-table tbody td.xlsx-selected {
background: var(--accent-bg);
}
.xlsx-table thead th.xlsx-hdr-selected,
.xlsx-table th.xlsx-rownum.xlsx-hdr-selected {
background: var(--accent-bg);
color: var(--accent-text, var(--accent, #4a90d9));
font-weight: 700;
}
/* #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.
#154-A1 — solid (not dashed) for a stronger active-cell affordance. */
@@ -11226,25 +11653,47 @@ body.desktop-mode .editor-container {
outline-offset: -2px;
}
/* #153 A7 — formula bar under the toolbar: [ A1 | > | input ] */
/* Barre d'édition de la cellule, juste au-dessus de la grille :
[ position | champ d'édition ] + recherche. Bandeau distinct du fond pour
rester identifiable dans tous les thèmes (le fond transparent la rendait
invisible). Elle vit DANS la barre d'outils sticky (dernier rang) : la
toolbar restait sinon peinte par-dessus au défilement. */
.xlsx-formula-bar {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
margin-bottom: 8px;
padding: 6px 8px;
background: var(--surface2, var(--surface));
border: 1px solid var(--border);
border-radius: 8px;
}
/* Dernier rang de la toolbar sticky : l'écart est déjà géré par son `gap`. */
.xlsx-toolbar .xlsx-formula-bar {
margin-bottom: 0;
}
/* Zone d'édition : position de la cellule | champ d'édition. */
.xlsx-editbar {
display: flex;
align-items: center;
gap: 6px;
flex: 1 1 auto;
min-width: 0;
}
/* #156-A7 — the Name Box is editable (“Atteindre”): a plain input styled like
the read-only badge it replaced. */
.xlsx-active-cell {
width: 84px;
flex: 0 0 auto;
box-sizing: border-box;
padding: 4px 8px;
border: 1px solid var(--border);
border: 1px solid var(--grid-border, var(--border));
border-radius: 4px;
background: var(--surface);
color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.8rem;
font-weight: 700;
text-align: center;
font-variant-numeric: tabular-nums;
}
@@ -11252,17 +11701,22 @@ body.desktop-mode .editor-container {
outline: 2px solid var(--accent, #4a90d9);
outline-offset: -2px;
}
.xlsx-formula-sep {
width: 14px;
height: 14px;
/* Pastille « fx » façon tableur : repère visuel du champ d'édition. */
.xlsx-edit-fx {
flex: 0 0 auto;
font-family: Georgia, 'Times New Roman', serif;
font-style: italic;
font-weight: 700;
font-size: 0.85rem;
color: var(--text-muted);
padding: 0 2px;
user-select: none;
}
.xlsx-formula-input {
flex: 1;
min-width: 0;
padding: 5px 10px;
border: 1px solid var(--border);
border: 1px solid var(--grid-border, var(--border));
border-radius: 4px;
background: var(--surface);
color: var(--text-primary);
@@ -11334,7 +11788,7 @@ body.desktop-mode .editor-container {
min-width: 200px;
max-width: 86vw;
padding: 4px;
background: var(--bg, #fff);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
@@ -11393,18 +11847,23 @@ body.desktop-mode .editor-container {
.xlsx-table tr.xlsx-frozen-row td,
.xlsx-table tr.xlsx-frozen-row th {
position: sticky;
top: 33px; /* thead height — keeps the frozen row below the header */
/* Hauteur de l'en-tête + décalage toolbar sticky (`--xlsx-stick`). */
top: calc(var(--xlsx-stick, 0px) + 33px); /* thead height — keeps the frozen row below the header */
z-index: 2;
background: var(--grid-header-bg);
background: var(--grid-header-bg, var(--bg-hover));
}
.xlsx-table td.xlsx-frozen-col {
position: sticky;
left: 44px; /* the row-number column width */
background: var(--grid-bg);
background: var(--grid-bg, var(--surface));
}
/* #153 A14 — structure menu (sheets / rows / columns) */
.xlsx-structure-menu {
/* #185 — the menu-bar dropdowns share the toolbar menus' look; the toolbar
menus keep the `.xlsx-structure-menu` class to themselves so `.xlsx-structure-menu`
queries (viewer, tests, E2E) only ever match the open toolbar menu. */
.xlsx-structure-menu,
.xlsx-menudrop {
position: absolute;
z-index: 30;
display: flex;
@@ -11412,7 +11871,8 @@ body.desktop-mode .editor-container {
gap: 2px;
min-width: 240px;
max-width: min(320px, 92vw);
margin-top: 4px;
/* Positioned under its toggle by anchorUnder() (viewer.js), which already
adds the 4px gap. */
padding: 6px;
border: 1px solid var(--border);
border-radius: 6px;
@@ -11610,6 +12070,29 @@ body.desktop-mode .editor-container {
background: var(--border);
}
/* Resize ghost: fixed guide line following the pointer while dragging a
header edge (column widths / row heights). */
.xlsx-resize-ghost {
position: fixed;
z-index: 10060;
margin: 0;
background: var(--accent, #4a90d9);
pointer-events: none;
}
.xlsx-resize-ghost[data-kind="col"] { width: 2px; }
.xlsx-resize-ghost[data-kind="row"] { height: 2px; }
/* Comment marker: content-free so copy/export/save text stays clean. */
.xlsx-comment-dot::after {
content: "\1F4AC";
font-size: 0.68em;
margin-left: 3px;
vertical-align: super;
cursor: help;
}
/* Computed formula links behave like spreadsheet links. */
.xlsx-formula-link { color: var(--accent, #4a90d9); text-decoration: underline; }
/* #179 — shared menu-entry icon (grid context menu, structure, format, export). */
.xlsx-menu-icon {
width: 14px;
@@ -11636,10 +12119,9 @@ body.desktop-mode .editor-container {
outline: 2px solid var(--accent, #4a90d9);
outline-offset: -2px;
}
/* #155 — Excel-like range selection + right-click / long-press context menu. */
.xlsx-table tbody td.xlsx-selected {
background: var(--accent-bg);
}
/* #155 — Excel-like range selection + right-click / long-press context menu.
Le remplissage de la sélection est déclaré plus haut et conditionné par
`.xlsx-range-multi` : pas de fond teinté pour une sélection d'une cellule. */
.xlsx-viewer.xlsx-selecting,
.xlsx-viewer.xlsx-selecting td {
user-select: none;
@@ -11673,7 +12155,8 @@ body.desktop-mode .editor-container {
.xlsx-context-menu {
min-width: min(260px, 86vw);
}
.xlsx-structure-menu {
.xlsx-structure-menu,
.xlsx-menudrop {
max-width: 92vw;
}
}
@@ -11703,6 +12186,203 @@ body.desktop-mode .editor-container {
background: var(--border);
}
/* #183 — Sheets-like colour panel (text + fill). The chrome follows the
active theme (variables only); the swatches carry their own colour inline,
which is data, not decoration. */
.xlsx-colorpick-wrap {
position: relative;
display: inline-flex;
}
.xlsx-color-native {
position: absolute;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
border: none;
padding: 0;
}
.xlsx-colour-panel {
position: absolute;
z-index: 40;
display: flex;
flex-direction: column;
width: 236px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
box-shadow: 0 6px 18px var(--shadow, rgba(0, 0, 0, 0.22));
font-size: 0.8rem;
}
.xlsx-colour-head {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.xlsx-colour-reset {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
font-size: 0.8rem;
cursor: pointer;
}
.xlsx-colour-reset:hover {
color: var(--accent);
border-color: var(--accent);
}
.xlsx-colour-icon {
width: 14px;
height: 14px;
flex: 0 0 auto;
}
/* 8 columns x 10 rows = the 80 curated swatches. */
.xlsx-colour-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
}
.xlsx-colour-swatch-btn {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
min-width: 20px;
padding: 0;
border: 1px solid var(--border);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.xlsx-colour-swatch-btn:hover {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}
.xlsx-colour-swatch-btn.selected {
box-shadow: 0 0 0 2px var(--accent);
}
.xlsx-colour-check {
width: 12px;
height: 12px;
pointer-events: none;
}
.xlsx-colour-section {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.xlsx-colour-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
color: var(--text-muted);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.xlsx-colour-row {
display: flex;
align-items: center;
gap: 6px;
}
.xlsx-colour-row .xlsx-colour-swatch-btn {
width: 22px;
height: 22px;
}
.xlsx-colour-add {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: 1px dashed var(--border);
border-radius: 50%;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
}
.xlsx-colour-add:hover {
color: var(--accent);
border-color: var(--accent);
}
.xlsx-colour-edit {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
}
.xlsx-colour-edit:hover {
color: var(--accent);
background: var(--bg-hover);
}
.xlsx-colour-foot {
margin-top: 10px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
.xlsx-colour-foot-item {
display: block;
width: 100%;
padding: 6px 4px;
border: none;
border-radius: 4px;
background: none;
color: var(--text-primary);
font-size: 0.8rem;
text-align: left;
cursor: pointer;
}
.xlsx-colour-foot-item:hover {
background: var(--bg-hover);
}
/* Mise en forme menu: the colour rows now carry a trigger + a live dot. */
.xlsx-format-colour {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
color: var(--text-primary);
font-size: 13px;
}
.xlsx-format-colour-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 4px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
cursor: pointer;
}
.xlsx-format-colour-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.xlsx-format-colour-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid var(--border);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-green, var(--accent)) 100%);
}
.xlsx-colour-panel {
max-height: 70vh;
overflow-y: auto;
}
/* JSDOM shims for the tests that click anchors */
mark {
font: inherit;
@@ -11785,7 +12465,7 @@ mark {
align-items: flex-start;
gap: 8px;
padding: 8px 10px;
margin-bottom: 8px;
margin: 8px 0;
border: 1px solid var(--border);
border-left: 3px solid var(--accent, #4a90d9);
border-radius: 4px;