From 435d3f4098d5cf4e38601dceb30bba5de327025a Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Wed, 8 Jul 2026 22:34:31 -0700 Subject: [PATCH] feat(observatory): reduced-motion + persistent pause control for the field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the launch guardrail: the field's ambient orbit + force-sim drift run >5s, so WCAG needs a persistent pause control and prefers-reduced- motion must be honored. - engine.setPaused(): freezes the deterministic clock so the orbit + sim drift hold still, while the frame still renders and the live preFrameHook still runs — discrete event pulses (firewall, decay, dream) are information, not decoration, so they land even when motion is reduced. - ObservatoryStage: a persistent ❚❚ PAUSE / ▶ RESUME control on the live field; auto-pauses under prefers-reduced-motion (user can override). Verified live: the pause button toggles engine.isPaused (frozen drift, pulses still land); check 0 err, 1043 tests, build green. Co-Authored-By: Claude Opus 4.8 --- .../lib/observatory/ObservatoryStage.svelte | 44 +++++++++++++++++++ apps/dashboard/src/lib/observatory/engine.ts | 26 ++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte index de7100e..aea355e 100644 --- a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte +++ b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte @@ -96,6 +96,32 @@ let projectionDays = $state(0); let liveBridge: LiveBridge | null = null; let liveDecayReady = $state(false); + + // Motion control (WCAG): the field's ambient orbit/sim drift runs >5s, so it + // needs a persistent pause control AND must honor prefers-reduced-motion. + // When paused the clock freezes (drift stops) but live event pulses still + // land — they are information, not decoration. Auto-pauses under + // reduced-motion; the user can still override with the button. + let paused = $state(false); + let userSetPause = $state(false); + function initReducedMotion() { + if (typeof window === 'undefined') return; + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + if (mq.matches && !userSetPause) paused = true; + const onChange = (e: MediaQueryListEvent) => { + if (!userSetPause) paused = e.matches; + }; + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + } + function togglePause() { + userSetPause = true; + paused = !paused; + } + // Push the pause state to the engine whenever either changes. + $effect(() => { + engine?.setPaused(paused); + }); // Live contradiction-firewall verdict — set when a real MemorySuppressed / // contradiction event quarantines a memory on camera. Held ~7s then fades. let liveFirewallLabel = $state(''); @@ -354,6 +380,7 @@ onMount(() => { loadGraph(); + return initReducedMotion(); }); @@ -409,6 +436,23 @@ {/if} + + {#if live} + + {/if} +