fix: flashs de navigation — la zone swapée n'est plus peint non montée (v7.45.3)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 1m58s
FlowDeck CI / test (push) Successful in 15m25s

- Symptôme : à chaque changement de page/section, une rafale de fenêtres /
  menus / états s'affiche une fraction de seconde avant la page voulue.
- Cause : pendant la fenêtre x-ignore (swap fdLoad → Alpine.initTree, mise en
  place en v7.45.2), .main-wrapper était peint NON montée : tous ses [x-show]
  visibles à leur valeur brute, puis masqués d'un coup au montage. Mesure
  (e2e/probe_nav_perf.spec.js sur l'instance locale) : 19 éléments bruts
  visibles 87 ms sur /library (CLS 0.149, sources lib-loading/lib-empty/
  lib-table), 80 éléments pendant 732 ms sur /settings, 7 sur /workspaces.
- Fix (app.js, handler htmx:afterSwap) : opacity:0 + pointer-events:none posés
  dans la même task que le swap (aucun paint intermédiaire possible), retirés
  APRÈS Alpine.initTree sur les 3 chemins de démontage (scripts chargés, zéro
  script, filet 4 s). reveal() est appelé avant les early-returns de done() →
  impossible de rester bloqué invisible.
- Après : 0 frame de contenu brut peint sur les 4 navigations du probe,
  1–2 frames masquées, zone révélée (opacity:1, pointer-events:auto) et
  montée (0 [x-show] non montés, x-ignore absent) à chaque fois.
- Gate : e2e/probe_nav_perf.spec.js — marques htmx, fenêtre x-ignore,
  échantillon par frame des [x-show] bruts peints, layout-shift + assertion
  (0 brut / zone révélée et montée), exécutable en CI.
- Reste (non traité, voir ROADMAP) : CLS résiduel 0.057 sur /library (états
  loading → table), fenêtre x-ignore de ~250 ms sur /settings (coût des
  scripts de page), topbar incluse dans .main-wrapper re-swappée à chaque nav.

Gates : probe_nav_perf + regression_partial_nav + regression_editor_mount +
regression_logout_dnd + smoke · pytest 1094 passed · ruff OK ·
OpenAPI 7.45.3.
This commit is contained in:
2026-10-03 00:15:41 -04:00
parent afbc236cc2
commit 1051a72b52
8 changed files with 191 additions and 4 deletions
+14
View File
@@ -356,9 +356,21 @@
if (!mw || mw === fdLastMw || mw.__fdSwapWatch) return;
fdLastMw = mw;
mw.__fdSwapWatch = true;
// Avant tout paint (même task que le swap) : la zone est x-ignore, donc non
// montée Alpine — tous ses [x-show] seraient peints à leur valeur brute
// (rafale de menus/panneaux/états visibles une fraction de seconde). On ne
// la peint qu'une fois initTree passé.
mw.style.opacity = '0';
mw.style.pointerEvents = 'none';
mw.setAttribute('x-ignore', '');
var pending = mw.querySelectorAll('script[src]').length;
var reveal = function () {
if (!mw.isConnected) return;
mw.style.opacity = '';
mw.style.pointerEvents = '';
};
var done = function () {
reveal(); // idempotent : appelé même si la branche suivante retourne
if (!mw.__fdSwapWatch) return;
mw.__fdSwapWatch = false;
document.removeEventListener('load', onScript, true);
@@ -367,6 +379,7 @@
mw.removeAttribute('x-ignore');
mw._x_ignore = false; // propriété posée par Alpine au checkpoint
try { Alpine.initTree(mw); } catch (e) { console.warn('[FlowDeck] mount', e); }
reveal();
};
var onScript = function (e) {
if (!e.target || e.target.tagName !== 'SCRIPT') return;
@@ -379,6 +392,7 @@
mw.__fdSwapWatch = false;
setTimeout(function () {
if (mw.isConnected) { try { Alpine.initTree(mw); } catch (e) { console.warn('[FlowDeck] mount', e); } }
reveal(); // après initTree : un paint max de blanc, jamais de contenu brut
}, 0);
return;
}