Skip to content

Motion: Proof Rail draw, approval lifecycle, scroll reveal (#541) - #542

Merged
richard-devbot merged 5 commits into
mainfrom
claude/motion-499
Aug 1, 2026
Merged

Motion: Proof Rail draw, approval lifecycle, scroll reveal (#541)#542
richard-devbot merged 5 commits into
mainfrom
claude/motion-499

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes #541. The three remaining zero-dependency children of epic #499 — Proof Rail transitions, approval gate lifecycle, scroll reveal. One bisected commit each, all TDD. The two open epic decisions (motion package, Rive) are untouched and were not needed.

The mechanism, and why it is a correctness property

Everything here uses transition, never @keyframes animation. That is not a style preference:

A transition fires only when a property changes on an element that already exists. An element inserted with its final value does not transition.

Which is exactly the epic's iron rule, expressed in CSS:

No JS guard, no state diffing, nothing for a future contributor to forget. Two browser assertions pin both directions, and they are deliberate opposites.

1. Proof Rail draws its check on a real PASS

The check is an SVG path present in every state, undrawn until the stage passes. A path rendered only for passed would be a new node at the flip, and a new node cannot transition — so the draw would never play on the one occasion it means something.

pathLength="1" makes the dash resolution-independent (the #525 idiom). --motion-draw was added by #512 and had no consumer until now.

Blocked and failed stages gain colour-and-shadow emphasis, with no box or layout property touched — this surface repaints every few seconds and a size change would shift the page under the reader's cursor.

Declined: "rail fill progress" from the epic child. Command Center already carries the #526 subway strip and the readiness KPIs; a third representation of stage progress on one page is noise. Flagged rather than silently dropped.

2. The approval lifecycle is legible when it changes

Transitions across PENDING → APPROVED → CONSUMED on the card that survives the morph.

Found in recon: .approval-card.consumed had no rule at all. A spent one-shot override lost its left border entirely and fell back to the bare card, while .pill.consumed was styled — so the state this epic singles out as deserving "a visible, dignified state change" had the least visual identity of any. It is now muted with a neutral border: a spent credential, deliberately distinct from the green of a live approval.

Correction to the epic: there is no STALE approval state in the Hub. The child says "CONSUMED / STALE". annotateApprovalLifecycle emits exactly one lifecycle value, and STALE_ARTIFACT_CHANGED appears nowhere under src/observability/dashboard/ — it is harness-side and never reaches the projection. Animating it would paint a state the server cannot produce, which is the #533 "approaching cap" prohibition again.

3. Scroll reveal — and the real bug it exposed

Two properties matter more than the animation.

The hidden state is applied by script, never by a static rule. There is deliberately no CSS on [data-reveal] itself. A reader whose JS fails, whose engine lacks IntersectionObserver, or who prefers reduced motion sees every section. A page whose job is showing governed state must never withhold it behind a capability check.

IntersectionObserver alone is not sufficient — the browser proved it. IO reports a change in intersection, so a section carried from below the fold to above it in one jump never has a non-zero ratio, is never reported, and stays hidden until the reader happens to scroll back. Jumping to the page bottom left four panels hidden at y=-1585 and y=-669. A passive scroll sweep now reveals anything that has reached the fold whether or not IO noticed, and detaches once nothing is pending.

I hit this because the first version of the browser journey timed out, and the honest read of a timeout is "find out why", not "raise the timeout".

Verification

  • 1964/1964 core, 51/51 browser (41 → 51), lint 0 errors, typecheck clean, validate 196 agents, security baseline OK, git diff --check clean.
  • Both directions of the honest-motion rule asserted: the flip animates on a provably surviving node (tagged before, checked after), the arrival does not.
  • Reduced motion asserted at the right moment — the global guard clamps transition-duration to .01ms rather than removing it, so the value still settles on the next frame, and the sampling window is what separates that from the .9s draw.
  • The scroll safety net is mutation-checked at both levels: removing it fails the unit test and the browser journey.
  • Live-verified on a three-run seeded hub, zero page errors; screenshots confirm the check drawing mid-flip and the three lifecycle states rendering distinctly.

A note on the one non-obvious failure

The #507 eslint canary failed mid-run with the genuine expand is not a function signature. Two causes stacked: my own three no-undef errors (the canary runs the real binary, so it is downstream of a clean tree), and a stale node_modules in this worktree still holding eslint 9.39.5 against a package.json asking for ^10. npm install resolved the second; no code change was needed for it.

Summary by CodeRabbit

  • New Features

    • Dashboard sections now reveal smoothly as they enter the viewport.
    • Proof-stage indicators display animated checkmarks and clearer failed or blocked states.
    • Approval cards show lifecycle transitions and a muted appearance after consumption.
  • Accessibility

    • Motion effects respect reduced-motion preferences.
    • Content remains visible when motion features are unsupported or unavailable.

richardsongunde and others added 4 commits August 1, 2026 16:34
The rail showed a static text glyph per state and nothing transitioned.

The check is now an SVG path present in EVERY state, undrawn until the stage
passes. That is the whole trick: a path rendered only for the passed state
would be a NEW node at the flip, and a new node cannot transition — the draw
would never play on the one occasion it means something. Keeping the node
alive through the morph lets its stroke-dashoffset transition instead.

The same property gives the honest-motion rule for free. A stage that
ARRIVES already passed renders its check statically, because a transition
needs a previous value to move from. Only an observed flip animates. No JS
guard, no state diffing.

pathLength="1" makes the dash resolution-independent — the #525 idiom — so
the offset reads as "how much of the check is undrawn".

Blocked and failed stages gain colour-and-shadow emphasis. No box or layout
property is touched: this surface repaints every few seconds and a size
change would shift the page under the reader's cursor.

--motion-draw was added by #512 and had no consumer until now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Approving is the one action in the Hub where a human's own decision changes
governed state, so the card should show that happening rather than silently
appearing in its new colour on the next repaint.

Transitions across border colour, opacity and background. Transition rather
than animation, so a card that merely renders in its final state on page
load stays still — only a real PENDING → APPROVED → CONSUMED move under
#495's morph animates.

Found while doing it: .approval-card.consumed had NO rule. A spent one-shot
override lost its left border entirely and fell back to the bare card, even
though .pill.consumed was styled — so the state this epic singles out as
deserving a dignified change had the least visual identity of any. It is now
muted with a neutral border: a spent credential, deliberately distinct from
the green of a live approval.

STALE is deliberately absent. The epic child says "CONSUMED / STALE", but
annotateApprovalLifecycle emits exactly one lifecycle value and
STALE_ARTIFACT_CHANGED appears nowhere in the dashboard — it is a
harness-side concept that never reaches the projection. Animating it would
paint a state the server cannot produce.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ent (#541)

Command Center sections reveal as they first reach the fold, once, with no
re-trigger on scroll-back.

Two properties matter more than the animation:

The hidden state is applied by script, never by a static rule. There is
deliberately no CSS on [data-reveal] itself — only .reveal-pending hides, and
only initScrollReveal adds it. A reader whose JS fails, whose engine lacks
IntersectionObserver, or who prefers reduced motion sees every section. A
page whose whole job is showing governed state must never withhold it behind
a capability check.

IntersectionObserver alone is NOT sufficient, which the browser proved: it
reports a CHANGE in intersection, so a section carried from below the fold to
above it in one jump never has a non-zero ratio, is never reported, and stays
hidden until the reader happens to scroll back. Jumping to the page bottom
left four panels hidden at y=-1585 and y=-669. A passive scroll sweep now
reveals anything that has reached the fold whether or not IO noticed, and
detaches itself once nothing is pending.

Both are mutation-checked — removing the sweep fails the unit test and the
browser journey.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three no-undef lint errors. Worth a note on how they surfaced: the #507
canary runs the REAL eslint binary over a brace glob, so my own lint errors
made it report a crash — the canary is downstream of a clean tree.

(The crash it actually printed was the genuine #507 signature, from a stale
node_modules in this worktree still holding eslint 9.39.5 against a
package.json asking for ^10. npm install resolved it; no code change.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 1, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b01fe8a3-0f1b-424b-b915-e6b42e5c304c

📥 Commits

Reviewing files that changed from the base of the PR and between 800c755 and a23a71d.

📒 Files selected for processing (3)
  • src/observability/dashboard/ui/lib.js
  • tests/browser/dashboard-541-motion.test.js
  • tests/hub-motion-499.test.js
📝 Walkthrough

Walkthrough

The dashboard adds proof-stage SVG transitions, approval lifecycle styling, and one-way scroll reveals. Motion behavior includes reduced-motion and unsupported-browser fallbacks. Browser and unit tests cover transitions, persistence, cleanup, and accessibility.

Changes

Dashboard motion behaviors

Layer / File(s) Summary
Proof-stage motion
src/observability/dashboard/ui/pages/command-center.js, src/observability/dashboard/ui/styles.js, tests/hub-motion-499.test.js, tests/browser/dashboard-541-motion.test.js
Proof stages use persistent SVG check paths, preserve non-passed glyphs, and apply transition-based passed, failed, and blocked states.
Approval lifecycle styling
src/observability/dashboard/ui/styles.js, tests/hub-motion-499.test.js, tests/browser/dashboard-541-motion.test.js
Approval cards transition lifecycle properties and use distinct styling for consumed approvals.
Scroll reveal integration
src/observability/dashboard/ui/lib.js, src/observability/dashboard/ui/client.js, src/observability/dashboard/ui/pages/index.js, src/observability/dashboard/ui/styles.js, tests/hub-motion-499.test.js, tests/browser/dashboard-541-motion.test.js
Marked dashboard sections reveal once through IntersectionObserver and scroll sweeps. Reduced motion, missing observers, cleanup, and accessibility behavior are covered.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 499 — Covers the motion-system children implemented here: proof-rail transitions, approval lifecycle animation, and scroll reveal.

Possibly related PRs

Suggested reviewers: richardsongunde

Poem

A rabbit checks the proof rail bright,
Draws SVG marks with gentle light.
Cards settle, consumed states rest,
Panels reveal when viewed their best.
Reduced motion keeps things still—
Hop, hop, shipped with careful skill!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Proof Rail, approval lifecycle, and scroll reveal changes.
Linked Issues check ✅ Passed The changes implement [#541]'s Proof Rail transitions, approval lifecycle styling, scroll reveal, motion safeguards, and related tests.
Out of Scope Changes check ✅ Passed The code and tests remain within [#541]'s stated scope, including the explicit exclusions for rail fill, STALE, motion, and Rive.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/motion-499

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Dashboard motion: Proof Rail check draw, approval lifecycle, scroll reveal

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Draw Proof Rail check via persistent SVG path transitioning only on PASS flips.
• Add approval card lifecycle transitions and restore distinct CONSUMED styling.
• Reveal Command Center sections on scroll without hiding content when JS/IO fails.
Diagram

graph TD
T["tests/browser/dashboard-541-motion.test.js"] --> CL["src/.../ui/client.js"] --> LIB["src/.../ui/lib.js (initScrollReveal)"] --> IDX["src/.../ui/pages/index.js ([data-reveal])"] --> CSS["src/.../ui/styles.js (transitions)"]
CC["src/.../ui/pages/command-center.js (Proof Rail)"] --> CSS
UT["tests/hub-motion-499.test.js"] --> LIB --> CSS
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. IntersectionObserver-only scroll reveal (no sweep safety net)
  • ➕ Less code; fewer event listeners.
  • ➕ Purely event-driven; no bounding-rect checks.
  • ➖ Can strand sections hidden on fast flicks / programmatic jumps (0→0 intersection ratio).
  • ➖ Risk of withholding critical governance content until user scrolls back.
2. Keyframe-based animations for draw/reveal
  • ➕ Simple to author and preview; can be visually richer.
  • ➖ Replays on initial render/arrival, violating the PR’s correctness rule (motion must represent observed change).
  • ➖ Harder to guarantee “no JS, no capability” fallback without accidental hidden content.

Recommendation: Keep the PR’s transition-driven approach. Using transitions on elements that already exist makes “motion only on observed change” a mechanism-level property (no JS guards to forget), and the scroll-reveal sweep prevents hidden panels on jump-scroll while still preserving the non-negotiable fallback: if JS/IO/reduced-motion blocks the feature, content remains visible.

Files changed (7) +622 / -10

Enhancement (5) +116 / -10
client.jsInitialize scroll reveal during dashboard startup +1/-0

Initialize scroll reveal during dashboard startup

• Calls initScrollReveal() alongside existing navigation and freshness initialization so reveal behavior applies immediately on load.

src/observability/dashboard/ui/client.js

lib.jsAdd IntersectionObserver-based one-way scroll reveal with safety sweep +60/-0

Add IntersectionObserver-based one-way scroll reveal with safety sweep

• Introduces initScrollReveal() to hide [data-reveal] targets only after JS confirms support and reduced-motion is not requested. Adds a scroll-driven sweep fallback to reveal panels that were jumped past without triggering IntersectionObserver callbacks, and detaches the scroll listener once all targets are revealed.

src/observability/dashboard/ui/lib.js

command-center.jsRender Proof Rail mark as persistent SVG check path +13/-1

Render Proof Rail mark as persistent SVG check path

• Replaces per-state glyph-only mark with a check SVG whose path exists in every state, enabling stroke-dashoffset transitions when a stage flips to passed. Keeps a glyph overlay for non-passed states while letting passed rely on the drawn check alone.

src/observability/dashboard/ui/pages/command-center.js

index.jsMark Command Center sections/panels as scroll-reveal targets +8/-8

Mark Command Center sections/panels as scroll-reveal targets

• Adds data-reveal to key long-page sections and panels so initScrollReveal() can progressively reveal them without changing baseline visibility when JS is unavailable.

src/observability/dashboard/ui/pages/index.js

styles.jsAdd transition-driven motion for approvals, Proof Rail, and scroll reveal +34/-1

Add transition-driven motion for approvals, Proof Rail, and scroll reveal

• Defines approval-card transitions (border-left-color/opacity/background) and adds a missing consumed card style. Adds .reveal-pending/.reveal-in styles with transitions, updates Proof Rail styling to animate the check draw via stroke-dashoffset, and emphasizes blocked/failed stages using color/shadow only to avoid layout shifts.

src/observability/dashboard/ui/styles.js

Tests (2) +506 / -0
dashboard-541-motion.test.jsAdd real-browser assertions for motion correctness and a11y +261/-0

Add real-browser assertions for motion correctness and a11y

• Adds browser tests proving the Proof Rail check animates only on a morph flip (same node) and does not animate on arrival or under reduced motion. Verifies scroll-reveal behavior (including jump-to-bottom), checks reduced-motion visibility, and runs axe to ensure no serious/critical violations on affected pages.

tests/browser/dashboard-541-motion.test.js

hub-motion-499.test.jsAdd unit tests for Proof Rail markup/CSS rules and scroll reveal logic +245/-0

Add unit tests for Proof Rail markup/CSS rules and scroll reveal logic

• Adds Node-based tests that assert the check SVG exists in every state, styling uses transitions (not animations) and consumes the shared motion token, and that blocked/failed emphasis avoids layout-affecting properties. Includes a harnessed test of initScrollReveal behavior, including reduced-motion and lack-of-IntersectionObserver fallbacks.

tests/hub-motion-499.test.js

@qodo-code-review

qodo-code-review Bot commented Aug 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (2)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Action required

1. Scroll reveal ignores #content ✓ Resolved 🐞 Bug ☼ Reliability
Description
initScrollReveal() only attaches its sweep fallback to the global window scroll event; when the
dashboard scrolls inside the #content overflow container, that fallback never runs and elements can
remain stuck in .reveal-pending (hidden). This can withhold governed data after programmatic jumps
or fast scrolls that IntersectionObserver doesn’t report.
Code

src/observability/dashboard/ui/lib.js[R77-123]

+function initScrollReveal() {
+  if (typeof IntersectionObserver === 'undefined') return;
+  if (typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+  var targets = document.querySelectorAll('[data-reveal]');
+  if (!targets || !targets.length) return;
+
+  var pending = [];
+  var observer = null;
+  var onScroll = null;
+
+  function reveal(node) {
+    var at = pending.indexOf(node);
+    if (at === -1) return;
+    pending.splice(at, 1);
+    node.classList.remove('reveal-pending');
+    node.classList.add('reveal-in');
+    if (observer) observer.unobserve(node);
+    if (!pending.length && onScroll) removeEventListener('scroll', onScroll);
+  }
+
+  // The safety net, and the reason this is not IntersectionObserver alone: a
+  // fast flick or a programmatic jump can carry a section from below the
+  // fold to above it without its intersection ratio ever leaving 0, so the
+  // observer is never called and the section stays hidden until the reader
+  // happens to scroll back. Verified: jumping to the page bottom left four
+  // panels hidden at y=-1585 and y=-669. Withholding real governance data
+  // because of a scroll gesture is not acceptable, so anything that has
+  // reached the fold is revealed whether or not IO noticed it.
+  function sweep() {
+    pending.slice().forEach(function(node) {
+      if (node.getBoundingClientRect().top < innerHeight) reveal(node);
+    });
+  }
+
+  onScroll = function() { sweep(); };
+  observer = new IntersectionObserver(function(entries) {
+    entries.forEach(function(entry) { if (entry.isIntersecting) reveal(entry.target); });
+    sweep();
+  }, { rootMargin: REVEAL_ROOT_MARGIN });
+
+  Array.prototype.forEach.call(targets, function(node) {
+    pending.push(node);
+    node.classList.add('reveal-pending');
+    observer.observe(node);
+  });
+  addEventListener('scroll', onScroll, { passive: true });
+}
Relevance

●● Moderate

No prior reviews found about scroll reveal needing #content scroll listeners; closest is general
lib.js robustness accepted/rejected.

PR-#504
PR-#520
PR-#216

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new implementation installs the safety-net sweep only via a global scroll listener. However, the
dashboard layout and navigation code indicate that the application can scroll inside #content
(overflow container) and explicitly resets #content.scrollTop, meaning scroll events occur on
#content, not window, so the sweep won’t run and pending elements can stay hidden.

src/observability/dashboard/ui/lib.js[65-123]
src/observability/dashboard/ui/styles.js[186-224]
src/observability/dashboard/ui/client.js[101-106]
src/observability/dashboard/ui/navigation.js[244-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`initScrollReveal()` uses a scroll-event “sweep” safety net, but it registers the listener only on `window` via the global `addEventListener('scroll', ...)`. In this dashboard, scrolling can occur on the `#content` element (an overflow container), so the safety net won’t run for `#content` scrolls and reveal targets can remain hidden.

### Issue Context
The layout defines `#content` as an overflow scroller and navigation explicitly manipulates `content.scrollTop`, which indicates that `#content` is a first-class scroll root in at least some layouts/browsers.

### Fix Focus Areas
- src/observability/dashboard/ui/lib.js[65-123]

### Implementation notes
- Determine the effective scroll roots (at minimum: `const content = document.getElementById('content')`).
- Register the sweep listener on both `window` and `content` (or detect which one is actually scrollable and attach there).
- Ensure cleanup removes listeners from the same target(s) they were attached to.
- Consider calling `sweep()` once after registering observers/classes to avoid any dependency on a scroll event for already-in-view sections.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Reveal tests scroll window 🐞 Bug ⚙ Maintainability
Description
The new browser tests use window.scrollTo() to trigger reveal, but the app can scroll within the
#content container; this may fail to exercise the production scroll path and can be flaky or miss
regressions depending on layout. Update the tests to scroll the same container(s) the app actually
uses.
Code

tests/browser/dashboard-541-motion.test.js[R204-225]

+motionTest('below-the-fold sections reveal on scroll and never re-hide (#541)', async ({ page, server }) => {
+  await open(page, server);
+
+  const pending = await page.evaluate(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
+  assert.ok(pending > 0, `something below the fold is waiting to reveal — got ${pending}`);
+
+  // A single jump to the bottom, deliberately: this is the case IO cannot
+  // see, because a section that passes from below the fold to above it never
+  // has a non-zero intersection ratio.
+  await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
+  await page.waitForFunction(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length === 0,
+  null, { timeout: PAGE_VISIBLE_TIMEOUT_MS });
+
+  // Scrolling back must not hide real data again.
+  await page.evaluate(() => window.scrollTo(0, 0));
+  await page.waitForTimeout(300);
+  const rehidden = await page.evaluate(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
+  assert.equal(rehidden, 0, 'nothing re-hides on scroll-back');
+});
Relevance

●● Moderate

No historical evidence requiring tests to scroll #content vs window; past test feedback focused on
other issues.

PR-#524
PR-#539
PR-#360

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The tests scroll the window to trigger reveal, but the dashboard is designed to support scrolling
via the #content container (overflow-y:auto) and navigation resets content.scrollTop, so window
scrolling may not represent the real user/programmatic scroll path that the reveal safety net must
handle.

tests/browser/dashboard-541-motion.test.js[204-225]
tests/browser/dashboard-541-motion.test.js[242-250]
src/observability/dashboard/ui/styles.js[186-224]
src/observability/dashboard/ui/client.js[101-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Browser tests use `window.scrollTo(...)` to drive scroll reveal. If the dashboard’s effective scroll container is `#content` (overflow scroller), these test actions won’t reliably trigger the reveal path they’re intended to validate.

### Issue Context
The app may scroll either `document.scrollingElement` or `#content` (navigation resets both). The tests should scroll the real container to ensure they validate the safety-net behavior.

### Fix Focus Areas
- tests/browser/dashboard-541-motion.test.js[204-225]
- tests/browser/dashboard-541-motion.test.js[242-250]

### Implementation notes
- Replace `window.scrollTo(...)` with something like:
 - `const c = document.getElementById('content'); if (c && c.scrollHeight > c.clientHeight) c.scrollTop = c.scrollHeight; else window.scrollTo(0, document.documentElement.scrollHeight);`
- Do the same for the scroll-back step.
- Consider adding an assertion for which scroller moved (optional) to make failures easier to interpret.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Motion tests missing screenshots 📘 Rule violation ▣ Testability
Description
The new browser tests assert computed styles and DOM state but do not perform screenshot-based
visual verification, so layout/visual regressions could slip through. This violates the requirement
that UI tests include automated screenshot comparison when validating visual correctness.
Code

tests/browser/dashboard-541-motion.test.js[R156-225]

+motionTest('a consumed override is visually distinct from a live approval (#541)', async ({ page, server }) => {
+  await open(page, server);
+  await navigate(page, 'approvals', { assertVisible: false });
+  await page.waitForFunction(() => document.querySelectorAll('#page-approvals .approval-card').length > 0,
+    null, { timeout: PAGE_VISIBLE_TIMEOUT_MS });
+
+  const styles = await page.evaluate(() => {
+    window.handleGlobalSnapshot = function () {};
+    const card = document.querySelector('#page-approvals .approval-card');
+    // Each state is read on its OWN fresh node. Re-classing one node and
+    // reading immediately samples the value mid-transition — the very
+    // transition this change adds — so the reads would lag one state behind
+    // and every state would look identical to its predecessor.
+    const read = (state) => {
+      const clone = card.cloneNode(false);
+      clone.className = 'approval-card ' + state;
+      card.parentNode.appendChild(clone);
+      const computed = getComputedStyle(clone);
+      const measured = { border: computed.borderLeftColor, opacity: computed.opacity };
+      clone.remove();
+      return measured;
+    };
+    return { asPending: read('pending'), asApproved: read('approved'), asConsumed: read('consumed') };
+  });
+
+  assert.notEqual(styles.asConsumed.border, styles.asApproved.border,
+    'a spent one-shot override does not look like a live approval');
+  assert.notEqual(styles.asConsumed.border, styles.asPending.border,
+    'nor like one still awaiting a decision');
+  assert.ok(Number(styles.asConsumed.opacity) < 1, 'and it recedes as settled business');
+});
+
+motionTest('an approval that changes state transitions on the same card (#541)', async ({ page, server }) => {
+  await open(page, server);
+  await navigate(page, 'approvals', { assertVisible: false });
+  await page.waitForFunction(() => document.querySelectorAll('#page-approvals .approval-card').length > 0,
+    null, { timeout: PAGE_VISIBLE_TIMEOUT_MS });
+
+  const declared = await page.evaluate(() => {
+    const card = document.querySelector('#page-approvals .approval-card');
+    return getComputedStyle(card).transitionProperty;
+  });
+  assert.match(declared, /border-left-color/, 'the card declares the lifecycle transition');
+  assert.match(declared, /opacity/, 'including the settling fade');
+});
+
+// ── scroll reveal ──────────────────────────────────────────────────────────
+
+motionTest('below-the-fold sections reveal on scroll and never re-hide (#541)', async ({ page, server }) => {
+  await open(page, server);
+
+  const pending = await page.evaluate(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
+  assert.ok(pending > 0, `something below the fold is waiting to reveal — got ${pending}`);
+
+  // A single jump to the bottom, deliberately: this is the case IO cannot
+  // see, because a section that passes from below the fold to above it never
+  // has a non-zero intersection ratio.
+  await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
+  await page.waitForFunction(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length === 0,
+  null, { timeout: PAGE_VISIBLE_TIMEOUT_MS });
+
+  // Scrolling back must not hide real data again.
+  await page.evaluate(() => window.scrollTo(0, 0));
+  await page.waitForTimeout(300);
+  const rehidden = await page.evaluate(() =>
+    document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
+  assert.equal(rehidden, 0, 'nothing re-hides on scroll-back');
+});
Relevance

● Weak

Repo browser tests rely on DOM/style assertions; no evidence of adopting screenshot-diff requirement
in reviews.

PR-#360
PR-#524
PR-#534

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2448668 requires UI tests that verify visual correctness to include
screenshot-based comparisons. The added browser tests validate visual differences via computed
styles and class changes, but do not capture/compare any screenshots.

Rule 2448668: UI tests must include screenshot-based visual verification
tests/browser/dashboard-541-motion.test.js[156-225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New UI browser tests validate visual behavior (e.g., “visually distinct”, reveal states) without taking and comparing screenshots, which fails the compliance requirement for screenshot-based visual verification.

## Issue Context
The suite uses `playwright-core` via `setupBrowserSuite()` and can capture screenshots from `page`, but currently has no baseline comparison step.

## Fix Focus Areas
- tests/browser/dashboard-541-motion.test.js[156-225]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. .proof-check animates stroke-dashoffset 📜 Skill insight ➹ Performance
Description
The new .proof-check styling transitions stroke-dashoffset, which violates the rule restricting
animations/transitions to transform and opacity. This can cause expensive paint work and is
non-compliant with the motion constraint.
Code

src/observability/dashboard/ui/styles.js[1902]

+.proof-check { width: 17px; height: 17px; fill: none; stroke: var(--green); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: stroke-dashoffset var(--motion-draw) var(--motion-ease-emphatic), opacity var(--motion-fast) var(--motion-ease); }
Relevance

● Weak

Similar SVG stroke-property transition complaints (e.g., stroke-dasharray) were rejected; team keeps
such transitions.

PR-#528
PR-#514
PR-#531

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399699 restricts CSS motion to transform and opacity. The .proof-check rule
introduces a stroke-dashoffset transition, which is neither transform nor opacity.

src/observability/dashboard/ui/styles.js[1902-1902]
Skill: design-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/observability/dashboard/ui/styles.js` adds a transition on `stroke-dashoffset` for `.proof-check`, but compliance requires motion to be limited to `transform` and `opacity`.

## Issue Context
The `.proof-check` rule uses `transition: stroke-dashoffset ...` to implement the draw effect.

## Fix Focus Areas
- src/observability/dashboard/ui/styles.js[1902-1902]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. .approval-card animates border-left-color 📜 Skill insight ➹ Performance
Description
The new .approval-card rule transitions border-left-color and background, which violates the
requirement that CSS motion be limited to transform and opacity. This can also trigger repaints
and degrade rendering performance on frequent UI updates.
Code

src/observability/dashboard/ui/styles.js[R1153-1156]

+.approval-card {
+  transition: border-left-color var(--motion-base) var(--motion-ease),
+              opacity var(--motion-base) var(--motion-ease),
+              background var(--motion-base) var(--motion-ease);
Relevance

● Weak

Motion-policy enforcement on non-transform/opacity transitions repeatedly rejected
(background/border-color kept).

PR-#520
PR-#531
PR-#539

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399699 requires that animated properties be limited to transform and opacity.
The new .approval-card rule explicitly transitions border-left-color and background in
addition to opacity.

src/observability/dashboard/ui/styles.js[1153-1156]
Skill: design-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/observability/dashboard/ui/styles.js` adds transitions on `border-left-color` and `background` for `.approval-card`, but compliance requires animations/transitions to be limited to `transform` and `opacity`.

## Issue Context
The new `.approval-card` transition list includes non-compliant properties (`border-left-color`, `background`).

## Fix Focus Areas
- src/observability/dashboard/ui/styles.js[1153-1156]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/observability/dashboard/ui/lib.js
Comment on lines +204 to +225
motionTest('below-the-fold sections reveal on scroll and never re-hide (#541)', async ({ page, server }) => {
await open(page, server);

const pending = await page.evaluate(() =>
document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
assert.ok(pending > 0, `something below the fold is waiting to reveal — got ${pending}`);

// A single jump to the bottom, deliberately: this is the case IO cannot
// see, because a section that passes from below the fold to above it never
// has a non-zero intersection ratio.
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForFunction(() =>
document.querySelectorAll('#page-command [data-reveal].reveal-pending').length === 0,
null, { timeout: PAGE_VISIBLE_TIMEOUT_MS });

// Scrolling back must not hide real data again.
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300);
const rehidden = await page.evaluate(() =>
document.querySelectorAll('#page-command [data-reveal].reveal-pending').length);
assert.equal(rehidden, 0, 'nothing re-hides on scroll-back');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Reveal tests scroll window 🐞 Bug ⚙ Maintainability

The new browser tests use window.scrollTo() to trigger reveal, but the app can scroll within the
#content container; this may fail to exercise the production scroll path and can be flaky or miss
regressions depending on layout. Update the tests to scroll the same container(s) the app actually
uses.
Agent Prompt
### Issue description
Browser tests use `window.scrollTo(...)` to drive scroll reveal. If the dashboard’s effective scroll container is `#content` (overflow scroller), these test actions won’t reliably trigger the reveal path they’re intended to validate.

### Issue Context
The app may scroll either `document.scrollingElement` or `#content` (navigation resets both). The tests should scroll the real container to ensure they validate the safety-net behavior.

### Fix Focus Areas
- tests/browser/dashboard-541-motion.test.js[204-225]
- tests/browser/dashboard-541-motion.test.js[242-250]

### Implementation notes
- Replace `window.scrollTo(...)` with something like:
  - `const c = document.getElementById('content'); if (c && c.scrollHeight > c.clientHeight) c.scrollTop = c.scrollHeight; else window.scrollTo(0, document.documentElement.scrollHeight);`
- Do the same for the scroll-back step.
- Consider adding an assertion for which scroller moved (optional) to make failures easier to interpret.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/observability/dashboard/ui/lib.js`:
- Around line 111-122: Update the reveal fallback setup around onScroll and the
IntersectionObserver to register the handler on the dashboard scroll owner,
document.getElementById('content'), instead of only the global event target.
Ensure the corresponding cleanup removes onScroll from the same content element,
preserving the existing passive listener behavior.

In `@src/observability/dashboard/ui/pages/index.js`:
- Line 44: Update the reveal-state handling for sections such as the
`overview-outcome` element so `.reveal-pending` also disables pointer/touch
interaction and keyboard focus for descendant controls, while preserving the
existing reduced-motion fallback behavior. Re-enable interaction when the
pending state is removed or the section is revealed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6593018-9e0d-4481-ac2c-06f70b3b3632

📥 Commits

Reviewing files that changed from the base of the PR and between 91fb336 and 800c755.

📒 Files selected for processing (7)
  • src/observability/dashboard/ui/client.js
  • src/observability/dashboard/ui/lib.js
  • src/observability/dashboard/ui/pages/command-center.js
  • src/observability/dashboard/ui/pages/index.js
  • src/observability/dashboard/ui/styles.js
  • tests/browser/dashboard-541-motion.test.js
  • tests/hub-motion-499.test.js

Comment thread src/observability/dashboard/ui/lib.js Outdated
const bodies = {
command: `
<section class="overview-decision-surface" id="overview-outcome" aria-labelledby="overview-outcome-title">
<section class="overview-decision-surface" data-reveal id="overview-outcome" aria-labelledby="overview-outcome-title">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 5 \
  'reveal-pending|reveal-in|\[data-reveal\]|inert|visibility|pointer-events|tabindex' \
  src/observability/dashboard/ui

Repository: richard-devbot/SDLC-rstack

Length of output: 36718


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,150p' src/observability/dashboard/ui/lib.js
sed -n '1780,1845p' src/observability/dashboard/ui/styles.js
sed -n '40,135p' src/observability/dashboard/ui/pages/index.js
sed -n '165,190p' src/observability/dashboard/ui/pages/command-center.js

Repository: richard-devbot/SDLC-rstack

Length of output: 18270


Make pending reveal sections non-interactive.

.reveal-pending only applies opacity: 0 and transform, so interactive children such as the Proof Rail “Open all evidence” button and the other new data-reveal sections remain focused and clickable while hidden. Add CSS or JS state that disables pointer/touch access and keyboard interaction while a section is pending without changing the reduced-motion fallback behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/observability/dashboard/ui/pages/index.js` at line 44, Update the
reveal-state handling for sections such as the `overview-outcome` element so
`.reveal-pending` also disables pointer/touch interaction and keyboard focus for
descendant controls, while preserving the existing reduced-motion fallback
behavior. Re-enable interaction when the pending state is removed or the section
is revealed.

Source: Coding guidelines

…weep (#541)

Qodo and CodeRabbit independently flagged that the scroll sweep listened on
window only, while #content carries overflow-y:auto. Scroll events do not
bubble, so a window listener sees only document scrolling.

Measured before changing anything: #content does NOT scroll today — its
ancestors use min-height, so the document grows instead. So this is a latent
fragility rather than a live bug. Fixed anyway, because the failure mode is
invisible governance data and the fix is one flag: the listener now captures
on document, so it sees scroll from whichever element ends up being the
scroller.

Correcting my own PR claim while I am here. I described the scroll sweep as
the thing that fixes the jump-past case and called it mutation-checked. The
mutation check removed BOTH sweep call sites. Isolating them shows they are
redundant: either alone fixes every case reproducible here, and only removing
both brings the bug back. They are kept deliberately — the IO call cannot
help when IO is never invoked, the scroll call cannot help when content moves
without a scroll — but "redundant belt-and-braces" is what they are, and the
comment now says so.

The new browser test is labelled for what it proves: reveal survives an
inner-container layout. It does not isolate the capture flag, because IO
fires regardless of which element scrolls — so it passes with capture
removed. The flag is asserted structurally in the unit suite instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Both reviewers found the same real thing — fixed, plus a correction to my own PR body

Qodo (#4, #5) and CodeRabbit independently flagged that the scroll sweep listened on window only while #content carries overflow-y: auto. Scroll events do not bubble, so a window listener sees only document scrolling.

What I measured before changing anything

#content does not scroll today. Its ancestors use min-height (#main, #shell), so the document grows and html/body is the scroller — scrollHeight - clientHeight on #content is exactly 0. So this is a latent fragility, not a live bug: one CSS change from min-height to height and the reveal would silently stop, hiding governed data.

Fixed anyway — the failure mode is invisible data and the fix is one flag. The listener now captures on document, so it sees scroll from whichever element ends up being the scroller, without enumerating containers.

Correcting my own PR body

While verifying the fix I found I had over-claimed, so I want to put it right rather than leave it standing.

I described the scroll sweep as the thing that fixes the jump-past case and called it mutation-checked. The mutation check removed both sweep call sites. Isolating them:

removed jump-past journey
scroll listener only passes
sweep-in-IO-callback only passes
both fails — the original bug

They are redundant. Either alone fixes every case reproducible here. I have kept both deliberately, because they fail in different directions — the IO call cannot help when IO is never invoked at all, the scroll call cannot help when content moves without a scroll (layout change, page switch, a panel growing) — but "belt-and-braces" is what they are, and the code comment now says exactly that instead of implying the scroll sweep is load-bearing on its own.

The new browser test is labelled for what it proves

reveal still fires when an inner container is the scroller pins that reveal survives that layout. It does not isolate the capture flag: IntersectionObserver fires regardless of which element scrolls, so the test passes with capture removed — I checked. Rather than dress it up as a mutation-proof it is not, the capture flag is asserted structurally in the unit suite, and the browser test's comment states its limit.

Verification

1965/1965 core, 9/9 in this suite (51 → 52 browser overall), lint 0 errors, typecheck clean.

Thanks to both — the finding was correct, and chasing it is what surfaced the redundancy I had mischaracterised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Motion: Proof Rail transitions, approval gate lifecycle, scroll reveal (#499 remaining zero-dep children)

2 participants