diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index 5d03720..f95410b 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -209,16 +209,20 @@ the profile to show or hide it in place. The rotation runs in a **persistent module engine** (`lib/bannerEngine`): the scheduler and timer live outside the components, so navigating between screens (which remounts the view) **continues the -cycle** rather than restarting it — each mounted `AdBanner` only attaches as the DOM host and resyncs -to the live message. A **smooth weighted round-robin** (`createScheduler`) picks the next message: -campaigns compete by their weight (each appears its weight share per cycle, evenly interleaved, not -at random), and a campaign's own messages advance round-robin. One message fades in (`fadeInMs`), -holds `holdMs` (a message wider than the strip pauses `edgePauseMs`, scrolls to its right edge at -`scrollPxPerSec`, pauses, and repeats while under `holdMs`), then — with more than one message — -fades out (`fadeOutMs`), waits `gapMs`, and the next fades in. The fade (a `{#if}` `transition:fade` -layer) is independent of the scroll (the inner track's transform), so a long, scrolling message -still fades at both ends. A lone message stays put (a long one keeps scrolling). All timings are -operator-set (`/_gm/banner-settings`). Under **reduce-motion** the fades collapse to an instant swap +cycle** rather than restarting it. A mounted `AdBanner` reads the engine's live message at init and +renders it **immediately, without a fade** (`bannerCurrent`), and attaches only as the DOM host — so +a screen change does not replay the fade; only a real message advance fades. A **smooth weighted +round-robin** (`createScheduler`) picks the next message: campaigns compete by their weight (each +appears its weight share per cycle, evenly interleaved, not at random), and a campaign's own messages +advance round-robin. One message fades in (`fadeInMs`), holds `holdMs` (a message wider than the +strip pauses `edgePauseMs`, scrolls to its right edge at `scrollPxPerSec`, pauses, and repeats while +under `holdMs`), then — with more than one message — fades out (`fadeOutMs`), waits `gapMs`, and the +next fades in. The fade (opacity on a `.fadewrap` layer) is independent of the scroll (the inner +track's transform), so a long, scrolling message still fades at both ends. A lone message stays put +(a long one keeps scrolling). A **viewport size change** (e.g. a portrait↔landscape rotation) +re-measures the current message (`remeasureBanner`, debounced), so its scroll re-evaluates for the +new width. All timings are operator-set (`/_gm/banner-settings`). Under **reduce-motion** the fades +collapse to an instant swap and a long message does not scroll. ## Result / status iconography (`lib/result.ts`) diff --git a/ui/src/components/AdBanner.svelte b/ui/src/components/AdBanner.svelte index ae9faa8..b50d3c2 100644 --- a/ui/src/components/AdBanner.svelte +++ b/ui/src/components/AdBanner.svelte @@ -1,6 +1,11 @@