feat(ads): client banner rotation, fade UX & live toggle (PR2) #71
Reference in New Issue
Block a user
Delete Branch "feature/ad-network-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PR2 of 2 for the server-driven advertising banner. PR1 (#70, merged) shipped the backend + admin + wire; this wires the UI and removes the gate. The banner is now live end-to-end.
What's here
lib/banner.ts—createScheduler: a smooth weighted round-robin over campaigns (each campaign appears its weight share per cycle, evenly interleaved — not random), round-robining a campaign's own messages. The rotator drives one message: fade-in → hold (scroll a long one) → fade-out → gap → fade-in the next. A lone message stays put (a long one keeps scrolling). Reduce-motion collapses the fades to an instant swap with no scroll.model.ts/codec.ts—Profile.banner(Banner/BannerCampaign/BannerTimings) decoded from the fbs block.Screen.svelte— the compile-timeSHOW_AD_BANNERgate is gone; the strip renders fromapp.profile.banner(campaigns + timings + reduce-motion).AdBanner.svelte— opacity-driven fades + scroll host; the rotator is recreated when the campaigns/timings change, so abannernotify re-fetch swaps them in place.app.svelte.ts— on thenotifybannersub-kind,refreshProfile()re-fetches the profile so the banner shows/hides in place.Verification (local, all green)
svelte-check(0 errors);test:unit252 (scheduler distribution + round-robin, fade sequence, single-message, reduce-motion,stop(), banner decode);build; Playwright e2e 69 passed on Chromium (CI runs WebKit too). The banner branch is inert in mock mode (noapp.profile.banner), so the existing e2e suite is unaffected.How to see it on the contour
Eligibility is
!paid && hint_balance == 0 && !no_banner— open as a guest (or a fresh free account) to be eligible. With only the seeded default campaign (one message) you'll see a single static tip; to watch the rotation + fades, add a timed campaign (or a second message to the default) and tune the timings in/_gm/banners+/_gm/banner-settings.Notes
Banner UX refinements (owner feedback): - Position: render the banner inside Header (under the title) instead of in Screen, so it sits in the same place on every screen. In the game the grown nav's spare height now falls below the banner (banner under title, board pinned to the bottom) — it no longer jumps to the game area. - Continuity: move the rotation into a persistent module engine (lib/bannerEngine) — the scheduler + timer live outside the components, so a navigation (which remounts the view) continues the cycle instead of restarting it. Each AdBanner only attaches as the DOM host and resyncs to the live message. - Fades: a long, scrolling message now fades at both ends. The fade is a {#if} transition:fade layer, independent of the scroll (the inner track's transform), so the two no longer interfere. Verified live (mock + Playwright): same position in lobby and game; the cycle continues across lobby↔game; opacity sampling shows fade-out + fade-in for the long message. Engine continuity unit-tested.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.