fix(ads): banner truly continuous across navigation + re-measure on resize
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s

The previous engine kept the scheduler running but the view re-`show()`-ed the
current message on every (re)mount, replaying the fade on each navigation — which
looked like the cycle restarting (especially for a single message). Now:

- A mounted AdBanner reads the engine's live message (bannerCurrent) and renders
  it immediately, with no fade; attach no longer re-shows. Only a real advance
  fades. Verified: opacity stays 1.0 across a navigation, message preserved.
- The fade is manual opacity on a .fadewrap layer (not transition:fade), kept
  independent of the scroll (inner track transform), so a long message still
  fades at both ends and a {#key} remount cannot force an intro fade.
- A viewport size change (portrait↔landscape) re-measures the current message
  (remeasureBanner on resize/orientationchange, debounced) so the scroll
  re-evaluates for the new width — the owner accepts the restart on resize.
  Rotator gains restart(); engine gains bannerCurrent()/remeasureBanner().

Engine continuity + remeasure unit-tested.
This commit is contained in:
Ilia Denisov
2026-06-16 05:45:38 +02:00
parent 3b20abe0bd
commit 5fb0daa746
5 changed files with 144 additions and 60 deletions
+8
View File
@@ -73,6 +73,9 @@ export interface BannerHost {
export interface Rotator {
start(): void;
stop(): void;
/** Re-present the current message (re-measure overflow + restart its scroll), e.g. after the
* viewport size changed. A no-op before start(). */
restart(): void;
}
/**
@@ -91,6 +94,7 @@ export function createBannerRotator(
const sched = createScheduler(campaigns);
let running = false;
let cycleStart = 0;
let lastShown = ''; // the message currently presented, for restart() (re-measure)
const timers: ReturnType<typeof setTimeout>[] = [];
const at = (ms: number, fn: () => void) => {
@@ -104,6 +108,7 @@ export function createBannerRotator(
function present(md: string) {
if (!running) return;
clear();
lastShown = md;
host.show(md);
at(config.fadeInMs, measure);
}
@@ -151,6 +156,9 @@ export function createBannerRotator(
running = false;
clear();
},
restart() {
if (running && lastShown) present(lastShown);
},
};
}