Promote development → master (initial production release: pre-release line + Stage 18) #104

Merged
developer merged 307 commits from development into master 2026-06-22 05:05:48 +00:00
2 changed files with 11 additions and 8 deletions
Showing only changes of commit 115c92b39a - Show all commits
+5 -3
View File
@@ -122,14 +122,16 @@ describe('createBannerRotator', () => {
r.stop(); r.stop();
}); });
it('shows a single message once and never fades it out', () => { it('pulses a lone message through the fade cycle (fades out, then back in)', () => {
vi.useFakeTimers(); vi.useFakeTimers();
const { host, calls, shown } = recordingHost(0); const { host, calls, shown } = recordingHost(0);
const r = createBannerRotator([{ weight: 1, messages: ['solo'] }], host, cfg); const r = createBannerRotator([{ weight: 1, messages: ['solo'] }], host, cfg);
r.start(); r.start();
vi.advanceTimersByTime(cfg.fadeInMs + cfg.holdMs * 5);
expect(shown).toEqual(['solo']); expect(shown).toEqual(['solo']);
expect(calls).not.toContain('hide'); vi.advanceTimersByTime(cfg.fadeInMs + cfg.holdMs); // hold elapses -> fade out
expect(calls).toContain('hide');
vi.advanceTimersByTime(cfg.fadeOutMs + cfg.gapMs + 1); // -> the same message fades back in
expect(shown).toEqual(['solo', 'solo']);
r.stop(); r.stop();
}); });
+6 -5
View File
@@ -85,8 +85,8 @@ export interface Rotator {
/** /**
* createBannerRotator rotates the scheduled messages: each message fades in, holds * createBannerRotator rotates the scheduled messages: each message fades in, holds
* `holdMs` (an overflowing one scrolls to its right edge and back while under holdMs), * `holdMs` (an overflowing one scrolls to its right edge and back while under holdMs),
* then — when more than one message exists — fades out over `fadeOutMs`, waits `gapMs`, * then fades out over `fadeOutMs`, waits `gapMs`, and fades the next one in. A lone
* and fades the next one in. A single message stays put (a long one keeps scrolling). * message pulses (the same message fades back in) so the cycle keeps its rhythm.
* Reduce-motion is handled by the host: pass zeroed fade timings and an overflowPx that * Reduce-motion is handled by the host: pass zeroed fade timings and an overflowPx that
* returns 0, and messages swap instantly without scrolling. * returns 0, and messages swap instantly without scrolling.
*/ */
@@ -125,8 +125,9 @@ export function createBannerRotator(
scrollCycle(over); scrollCycle(over);
return; return;
} }
if (sched.total > 1) at(config.holdMs, advance); // Hold, then run the fade cycle. A lone message pulses (fades out, then back in) rather than
// A single fitting message stays put. // sitting frozen, so the banner keeps its rhythm even with a single campaign message.
at(config.holdMs, advance);
} }
function scrollCycle(over: number) { function scrollCycle(over: number) {
@@ -134,7 +135,7 @@ export function createBannerRotator(
at(config.edgePauseMs, () => { at(config.edgePauseMs, () => {
host.scrollTo(over, dur); host.scrollTo(over, dur);
at(dur + config.edgePauseMs, () => { at(dur + config.edgePauseMs, () => {
if (sched.total > 1 && Date.now() - cycleStart >= config.holdMs) { if (Date.now() - cycleStart >= config.holdMs) {
advance(); advance();
} else { } else {
host.resetScroll(); host.resetScroll();