perf(admin): infinite-scroll the contenus langues/mots-clés tables

The contenus page loaded both lookup tables in full via HTMX on page load:
760 tag rows + 216 language rows, ~2.4MB of HTML including ~1960 inline
SVG icons and per-row CSRF forms. The DB queries were already fast (~6ms);
the cost was pure client-side payload and DOM.

- Add paged lookups: getTagsPage/getLanguagesPage + countTagsWithCount/
  countLanguagesWithCount, and optional limit/offset on the existing
  unpaged variants (backward compatible).
- Fragments serve 25 rows per request. A sentinel <tr> with
  hx-trigger="intersect once root:#<wrap>" appends the next page when it
  scrolls into the table's own scroll container (htmx 'revealed' only
  checks the window viewport, not nested scrollers).
- Search forms still swap the whole wrapper and reset to offset 0.
- Style the load-more row; add PagedLanguagesTagsTest coverage.

Initial DOM for the page drops from ~2.4MB to ~160KB; scrolling reaches the
full totals (736 tags, 216 languages) and stops cleanly.
This commit is contained in:
Pontoporeia
2026-09-18 16:26:49 +02:00
parent 8016712741
commit 25c5133086
6 changed files with 475 additions and 62 deletions
+16
View File
@@ -255,6 +255,22 @@
text-align: center;
}
/* "Load more" sentinel row for infinite-scroll admin tables (contenus).
Sits at the bottom of the <tbody>; htmx replaces it with the next page
of rows when it scrolls into the table's viewport. */
.admin-load-more__cell {
color: var(--text-tertiary);
padding: var(--space-s) var(--space-2xs);
text-align: center;
font-size: var(--step--1);
}
/* Fade the sentinel out once its request is in flight so it does not
visually "stick" while the next page is fetched. */
.admin-load-more.htmx-request .admin-load-more__cell {
opacity: 0.4;
}
/* Identifier column in the thesis table */
.admin-table-id {
color: var(--text-secondary);