Fix table jitter, horizontal scroll, and vertical scroll snapping on /lignes

Root cause: the main table used auto layout, so every loadOlder()/
loadNewer() reflowed every column's width based on whatever was
currently loaded -- visibly shifting the table, spilling past the
viewport into a horizontal scrollbar, and corrupting loadOlder()'s
scroll-position compensation (which assumes the scrollHeight delta
after prepending rows is *only* the new rows' own height -- not true
once existing rows also reflow). That's what made scrolling up feel
like it kept snapping back down.

- table-layout: fixed with explicit per-column widths (percentages
  throughout, not mixed with rem -- mixing meant the rem columns' width
  was added on top of the percentage budget instead of coming out of
  it), plus box-sizing: border-box so padding doesn't inflate columns
  beyond their declared width.
- overflow-x: hidden instead of auto on the scroll container: with both
  x and y auto on the same element, the browser has to guess whether a
  vertical scrollbar will appear before laying out width: 100%, and a
  wrong guess understates available width by a scrollbar's worth --
  exactly enough to tip a tightly-fitting table into needing horizontal
  scroll too.
- Fixed a footer-row column count bug found along the way: it still had
  two actions-col cells and a colspan=5 label from before the link
  button was removed, leaving the label 6 columns short of Signalement
  and misaligning every footer cell after it.

Verified with a 25-round scripted scroll-up stress test spanning 3
loadOlder() triggers: scroll position stays correctly anchored near
where the user was looking, never jumps toward the bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 15:16:06 +02:00
co-authored by Claude Sonnet 5
parent eda79e75a3
commit fa28783bcc
2 changed files with 73 additions and 9 deletions
@@ -159,24 +159,49 @@ html.gin--dark-mode #figli-home-app {
}
#figli-home-app .figli-table-wrap {
overflow-x: auto;
/* Fixed columns below are sized to always fit -- hidden (not auto) so
there's never a horizontal scrollbar to fight the vertical one for
layout space. With both set to auto, the browser has to guess
whether a vertical scrollbar will appear before it can lay out the
table's width:100%, and getting that guess wrong understates the
available width by one scrollbar's worth, which was enough to tip
this from "just barely fits" into "needs to scroll horizontally
too" -- and any actual residual overflow (a column pushed a few px
over by content this doesn't attempt to break) is silently clipped
here instead of surfacing a scrollbar for it. */
overflow-x: hidden;
border: 1px solid var(--figli-border);
border-radius: 6px;
max-height: 75vh;
overflow-y: auto;
}
/* table-layout: fixed -- auto layout was recomputing every column's
width from whatever happened to be currently loaded, so the table
visibly reflowed (and drifted wider than the viewport, forcing
horizontal scroll) every time loadOlder()/loadNewer() brought in rows
with different content. Fixed layout locks each column to the widths
set below regardless of content, so the table's total width never
changes -- no more horizontal scroll, and loadOlder()'s scroll
compensation (which assumes the height delta after prepending is
*only* the new rows' own height) is now accurate again, since existing
rows no longer reflow when new ones are added. */
#figli-home-app table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
background: var(--figli-bg);
color: var(--figli-text);
font-size: 0.8rem;
white-space: nowrap;
}
#figli-home-app th,
#figli-home-app td {
/* border-box -- otherwise each column's percentage width (fixed
layout, above) sets only the content box, and this cell's own
padding/border get added on top of it, so 18 columns' worth of
padding quietly pushes the table wider than 100% again. */
box-sizing: border-box;
text-align: left;
padding: 0.3rem 0.6rem;
border-bottom: 1px solid var(--figli-border);
@@ -201,16 +226,16 @@ html.gin--dark-mode #figli-home-app {
font-variant-numeric: tabular-nums;
}
#figli-home-app th:nth-child(6),
#figli-home-app td.figli-libelle {
width: 15%;
white-space: normal;
min-width: 220px;
max-width: 340px;
}
#figli-home-app th:nth-child(7),
#figli-home-app td.figli-flag-cell {
width: 8%;
white-space: normal;
min-width: 120px;
max-width: 220px;
}
#figli-home-app tr.figli-group-row td {
@@ -378,12 +403,52 @@ html.gin--dark-mode #figli-home-app {
table's default 0.6rem horizontal cell padding. */
#figli-home-app td.actions-col,
#figli-home-app th.actions-col {
width: 1%;
width: 3%;
padding: 0.2rem 0.1rem;
text-align: center;
white-space: nowrap;
}
/* Column widths for the fixed layout above -- sized so the widest
realistic content (longest client/type label, an 8-figure amount)
fits without pushing the table past 100%, while the two free-text
columns (Client, Libellé/Détail) get most of the remaining room.
Every column is a percentage (including actions-col/Date above,
deliberately not rem) -- mixing units meant the rem columns' pixel
width was added *on top of* the percentage budget instead of coming
out of it, silently pushing the table a few pixels past 100% and
reintroducing the horizontal scrollbar this is meant to avoid.
Percentages intentionally sum to a little under 100%: table-layout:
fixed treats them as relative weights, not a hard budget, so slightly
under leaves headroom rather than risking every column getting
scaled down to fit. */
#figli-home-app th:nth-child(2),
#figli-home-app td:nth-child(2) {
width: 6%;
white-space: nowrap;
}
#figli-home-app th:nth-child(3),
#figli-home-app td:nth-child(3) {
width: 10%;
}
#figli-home-app th:nth-child(4),
#figli-home-app td:nth-child(4) {
width: 7%;
}
#figli-home-app th:nth-child(5),
#figli-home-app td:nth-child(5) {
width: 5.5%;
white-space: nowrap;
}
#figli-home-app .amount:not(.compte-col) {
width: 5.5%;
white-space: nowrap;
}
#figli-home-app .amount.compte-col {
width: 3.5%;
white-space: nowrap;
}
/* Buttons stay inline (not flex) inside the <td> -- display: flex on a
table cell breaks the table's own column-width calculation (observed:
a ~55px gap opening up between this column and the next, and the
@@ -250,8 +250,7 @@
<tfoot>
<tr class="figli-totals-row">
<td class="actions-col"></td>
<td class="actions-col"></td>
<td colspan="5">
<td colspan="6">
Solde {{ currentYear || '…' }} (créditeur / débiteur)
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
</td>