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
+26 -12
View File
@@ -296,23 +296,37 @@
autocomplete="off"
maxlength="4000"
></textarea>
<!-- Phase 117 (owner request 2026-09-15, live-mockup-confirmed):
the persistent in-turn loader — a BRAIN-WAVE: a compact ECG
trace (P bump, QRS spike, T bump — 49px wide, 8px flat
tails; the owner picked it over the first equalizer mock).
.bw-ghost is the always-visible dim trace; .bw-pulse is the
brand sweep — a stroke-dasharray segment that travels the
path (styles.css bwdraw, pathLength-normalized for a
seamless loop). Positioned LEFT of the button on purpose:
the row is flex-end aligned with a flex:1 textarea, so the
loader's appearance and disappearance eat the textarea's
space and the button NEVER shifts (the owner's no-move
requirement). The phase-109 D16 contract stands: DECORATIVE
(aria-hidden) — #send-status stays the sole a11y announcer,
so the state's meaning never moves; hidden by default (idle
on load); app.js's setUiState is the SOLE writer of its
hidden attribute — shown iff the turn is in flight, hidden
by construction in every terminal state (PLAN §7.4 never-
stale, no per-handler cleanup). Never constructed in JS: the
createElement/textContent house rule — the SVG is static
markup too. -->
<div id="turn-loader" class="turn-loader" aria-hidden="true" hidden>
<svg class="bw" viewBox="0 0 49 32">
<path class="bw-ghost" d="M0 16 H8 L15 12 L18 16 L21 18 L24 3 L27 29 L30 16 H35 L38 11 L41 16 H49"></path>
<path class="bw-pulse" d="M0 16 H8 L15 12 L18 16 L21 18 L24 3 L27 29 L30 16 H35 L38 11 L41 16 H49" pathLength="140"></path>
</svg>
</div>
<button type="submit" class="send-btn" id="send-btn">
<span class="spinner" aria-hidden="true" hidden></span>
<span class="btn-label" id="send-label">Send</span>
<span class="visually-hidden" aria-live="polite" id="send-status"></span>
</button>
<!-- Phase 109 (TODO.md L3, D16): the persistent in-turn loader —
ONE static element in the composer's status row, the
visible companion of the #send-status line. It is
DECORATIVE (aria-hidden): #send-status stays the sole a11y
announcer, so the state's meaning never moves (the L1791
house pattern). Hidden by default (idle on load); app.js's
setUiState is the SOLE writer of its hidden attribute —
shown iff the turn is in flight, hidden by construction in
every terminal state (PLAN §7.4 never-stale, no
per-handler cleanup). Never constructed in JS: the
createElement/textContent house rule. -->
<div id="turn-loader" class="turn-loader" aria-hidden="true" hidden></div>
</form>
</div>
</div>