feat(ui): swap the in-turn loader for a brain-wave sweep left of the send button
Build and Push Containers / build-and-push-app (push) Successful in 2m11s
Build and Push Containers / build-and-push-db (push) Successful in 13s

Phase 117 (owner request, live-mockup-confirmed): the phase-109 3-dot
cue becomes a compact ECG trace (49px, P/QRS/T) with a brand sweep
traveling the path (bwdraw, 42/140 dash segment, 0.9s loop), the
loader repositioned left of the button so its appearance never shifts
it. setUiState stays the sole owner of the loader's hidden attribute;
the reduced-motion variant stills the sweep. Unit + lifecycle-E2E pins
updated for the new contract.
This commit is contained in:
2026-09-15 11:55:59 -04:00
parent adf31a4a35
commit 2ac3fc89c2
4 changed files with 209 additions and 101 deletions
+18 -9
View File
@@ -54,8 +54,9 @@ context, ``_reset_db`` reseeds the KB deterministically):
``SEND_STATUS`` idle shape (empty — not stuck on a mid-turn label);
then, in a context with ``reducedMotion: "reduce"`` (the Playwright
context option), a second turn shows the loader STILL visible
mid-turn — the reduced-motion variant stills the dots (computed
``animation-name: none``), it does not HIDE the cue: visibility is
mid-turn — the reduced-motion variant stills the sweep (the phase-
117 brain wave: computed ``animation-name: none`` on the ``.bw-pulse``
path), it does not HIDE the cue: visibility is
owned by the JS ``hidden`` attribute in ``setUiState`` (D16), not by
CSS.
"""
@@ -281,13 +282,20 @@ def test_loader_a11y_and_reduced_motion(
status = page.locator("#send-status")
expect(status).to_have_attribute("aria-live", "polite")
expect(status).to_have_text("")
# Control (default-motion context): the loader's dots run the
# typing animation — the reduced-motion check below stills it.
# Control (default-motion context): the CONTAINER carries no
# animation — the SWEEP path runs the phase-117 bwdraw travel —
# the reduced-motion check below stills it.
assert (
page.evaluate(
"getComputedStyle(document.querySelector('#turn-loader')).animationName"
)
== "typing"
== "none"
)
assert (
page.evaluate(
"getComputedStyle(document.querySelector('#turn-loader .bw-pulse')).animationName"
)
== "bwdraw"
)
# Reduced motion: a FRESH context with ``reducedMotion: "reduce"``
@@ -308,12 +316,13 @@ def test_loader_a11y_and_reduced_motion(
# loader is still the visible cue ...
expect(rm_page.locator(".tool-call")).to_be_visible(timeout=30_000)
expect(rm_page.locator("#turn-loader")).to_be_visible()
# ... and the reduced-motion CSS variant IS applied: static
# dots, no pulse (the animation is gone from the computed
# style — the element itself is unchanged).
# ... and the reduced-motion CSS variant IS applied: the full
# trace shows static, no travel (the animation is gone from
# the sweep path's computed style — the loader itself is
# unchanged).
assert (
rm_page.evaluate(
"getComputedStyle(document.querySelector('#turn-loader')).animationName"
"getComputedStyle(document.querySelector('#turn-loader .bw-pulse')).animationName"
)
== "none"
)