Skip to content

feat(bulma-ui): add Reveal component for scroll-triggered animations - #247

Closed
bestaxbot wants to merge 2 commits into
mainfrom
claude/issue-197-20260707-0617
Closed

bestaxbot wants to merge 2 commits into
mainfrom
claude/issue-197-20260707-0617

Conversation

@bestaxbot

@bestaxbot bestaxbot commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a Reveal component: a lightweight, CSS-driven wrapper backed by
IntersectionObserver that animates content into view as it scrolls into the
viewport (fade, fade-up, fade-down, slide-left, slide-right, zoom,
flip), with an optional cascade mode that staggers direct children with an
incrementing delay.

Accessibility/progressive-enhancement are built in, not opt-in:

  • Automatically skips the animation and renders the final, visible state when
    the user prefers reduced motion (prefers-reduced-motion: reduce).
  • Renders the final, visible state during SSR and on the first client render,
    so content is never hidden if JavaScript never runs (crawlers, no-JS).
  • Falls back to the visible state immediately when IntersectionObserver
    isn't available.

Passing a plain (non-forwardRef) component as as — which is how every
component in this library, including Section/Card, is written — would
otherwise silently break scroll detection, since the ref never attaches and
content would stay hidden forever. This is handled by falling back to an
internal wrapper div for scroll observation whenever as isn't a plain
intrinsic HTML tag, with a regression test covering it
(Reveal.test.tsx: "still observes and reveals when as is a non-forwardRef
component").

Fixes #197

Changes

  • bulma-ui/src/components/Reveal.tsx — the component
  • bulma-ui/src/components/Reveal.stories.tsx — Storybook stories (including
    a scroll-to-reveal demo)
  • bulma-ui/src/components/__tests__/Reveal.test.tsx — 22 tests, 100% coverage
  • bulma-ui/src/scss/components/_reveal.scss — animation styles, registered
    in _index.scss
  • docs/docs/api/components/reveal.md — API docs page
  • bulma-ui/src/index.ts — public export
  • skills/bestax-custom-component/references/component-catalog.md —
    regenerated via pnpm gen:catalog

Test plan

  • pnpm --filter @allxsmith/bestax-bulma exec tsc --noEmit
  • pnpm --filter @allxsmith/bestax-bulma run lint
  • pnpm --filter @allxsmith/bestax-bulma run test:coverage (100% on
    Reveal.tsx, no threshold regressions; 3350 tests passing)
  • pnpm run format:check
  • pnpm run gen:catalog:check

Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new Reveal component for scroll-triggered entrance animations, including cascade staggering, once behavior, and flexible rendering via the as prop.
    • Added Storybook stories showcasing default animations, all variants, section usage, cascading examples, and scroll-to-reveal behavior.
  • Bug Fixes
    • Improved progressive enhancement: respects reduced-motion preferences and safely falls back when scroll observation isn’t supported.
  • Documentation
    • Added API documentation for Reveal and updated the component catalog to include it.
  • Tests
    • Added a comprehensive test suite covering rendering modes, intersection behavior, cascade timing, reduced-motion, and SSR safety.

Adds a Reveal component: a lightweight, CSS-driven wrapper backed by
IntersectionObserver that animates content into view as it scrolls into
the viewport (fade, fade-up, fade-down, slide-left, slide-right, zoom,
flip), with an optional cascade mode that staggers direct children.

Fixes #197

Co-authored-by: Alex Smith <allxsmith@users.noreply.github.com>
@bestaxbot bestaxbot added the ai-loop AI-authored PR in the autonomous review/fix loop label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b83e42cd-93fc-44bb-8bff-da7c318cf920

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa5d6b and eb781a7.

📒 Files selected for processing (3)
  • bulma-ui/src/components/Reveal.tsx
  • bulma-ui/src/components/__tests__/Reveal.test.tsx
  • docs/docs/api/components/reveal.md
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/api/components/reveal.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • bulma-ui/src/components/tests/Reveal.test.tsx

Walkthrough

Adds a new Reveal component to bulma-ui that animates content into view on scroll, with reduced-motion and SSR-safe fallbacks, cascade staggering, SCSS styles, tests, stories, docs, and package exports.

Changes

Reveal Component

Layer / File(s) Summary
Reveal props and animation type contract
bulma-ui/src/components/Reveal.tsx
Defines RevealAnimation and RevealProps with animation, timing, cascade, and as props.
Reduced-motion hook and observer effects
bulma-ui/src/components/Reveal.tsx
Adds the reduced-motion hook and observer-driven reveal state with SSR-safe defaults and no-observer fallback behavior.
Class computation, cascade rendering, and ref wiring
bulma-ui/src/components/Reveal.tsx, bulma-ui/src/index.ts
Computes reveal classes/styles, staggers cascade children, wires refs for intrinsic vs custom as values, and re-exports Reveal from the package entrypoint.
Reveal SCSS styles
bulma-ui/src/scss/components/_reveal.scss, bulma-ui/src/scss/components/_index.scss
Adds reveal animation styles, visible-state and reduced-motion rules, and registers the stylesheet in the components index.
Reveal test suite
bulma-ui/src/components/__tests__/Reveal.test.tsx
Adds tests for rendering, observer behavior, reduced motion, cascade handling, and SSR output.
Storybook stories, documentation, and catalog entry
bulma-ui/src/components/Reveal.stories.tsx, docs/docs/api/components/reveal.md, skills/bestax-custom-component/references/component-catalog.md
Adds Storybook examples, API documentation, and a component-catalog entry for Reveal.

Estimated code review effort: 3 (Moderate) | ~30 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the new Reveal component and its scroll-triggered animation behavior.
Description check ✅ Passed The description covers the summary, linked issue, changed files, and test plan, though the template’s package and checklist sections are not fully filled.
Linked Issues check ✅ Passed The PR implements the Reveal component, animations, cascade, reduced-motion, SSR safety, and props required by #197.
Out of Scope Changes check ✅ Passed All changes support the Reveal feature or its docs, stories, tests, styles, export, and catalog entry, with no unrelated work evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-197-20260707-0617

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

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://91707a07.bestax.pages.dev

Comment on lines +243 to +246
return (
<div ref={setNode} className={combinedClasses} style={wrapperStyle}>
<Component {...rest}>{content}</Component>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Custom-component as splits props across two elements — 🟡 Minor · API

What: When as is a component (not a string), className, style, and the Bulma helper classes land on the internal wrapper div, while everything else in ...rest (id, aria-*, data-*, event handlers) lands on the inner <Component>. For the string-as path (line 231) all of these are unified on the single rendered element.

Why it matters: This is inconsistent with the rest of the library, where a polymorphic as puts the full prop set on one element. A caller writing <Reveal as={Section} id="hero" className="highlight" m="4"> gets id on the <section> but highlight/m-4 on the wrapper div — so a combined selector like #hero.highlight matches nothing, and consumers can't reason about which element carries their class/style vs. their id/aria. It's easy to hit without realizing why styling "doesn't apply."

Why it matters less: Keeping id/aria/events on the semantic inner element is arguably correct, and moving the animation classes off the observed node isn't trivial. So this may be an intentional trade-off — but it should at least be documented on the as prop (in the docs page's props table and/or the JSDoc) so the asymmetry isn't a surprise.

Prop distribution for as={Section}
flowchart TD
    R["&lt;Reveal as={Section} id className style m='4' onClick&gt;"]
    R --> D["div.reveal (observed node)\nclassName + helper classes + style"]
    D --> S["Section\n...rest: id, aria-*, data-*, onClick"]
Loading

No code change strictly required; recommend a one-line note on the as prop describing where user className/style land when a component is passed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in eb781a7 — I took the documentation route you recommended rather than restructuring the render, since keeping id/aria/events on the semantic inner element (and the animation classes on the observed node) is the intentional trade-off here, not a bug.

I documented the asymmetry in two places:

  • The as prop's JSDoc in Reveal.tsx now spells out that for a plain intrinsic tag everything lands on the single element, whereas for a component className/style/helper classes go on the observed wrapper div and ...rest (id, aria-*, data-*, event handlers) is forwarded to the inner component.
  • The docs page (docs/docs/api/components/reveal.md) gains a :::note right under the "Rendering as a different element" example making the same point, and it explicitly calls out your #hero.highlight gotcha — a combined selector assuming id and className sit on the same element won't match when as is a component.

I left the runtime behavior unchanged: moving the animation classes off the observed node isn't free, and unifying every prop onto the inner component would require ref-forwarding that this library's components don't provide, which is exactly the fallback the wrapper div exists to handle.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review — 1 finding

# Severity Area Finding Location
1 🟡 Minor API Custom-component as splits className/style/helper-classes (wrapper div) from id/aria/data/events (inner component); inconsistent with the rest of the library and undocumented bulma-ui/src/components/Reveal.tsx:243

Overall: The change is sound and unusually complete for a new component — it ships all five anatomy artifacts (component, tests, story, docs page, export) plus the SCSS partial and a regenerated skill catalog, and lands 100% statement/branch/function/line coverage on Reveal.tsx (23 passing tests). The genuinely hard parts are handled well: the isMounted gate keeps SSR/first-client render identical so there's no hydration mismatch, prefers-reduced-motion and the IntersectionObserver-absent case both fall back to the visible final state, and the callback-ref-as-state pattern correctly re-runs the observer effect once the node attaches. The riskiest/most subtle area is the polymorphic as handling — where the human should focus first — but the only real wrinkle there is the prop-distribution asymmetry noted above, which is arguably intentional and just needs a doc note rather than a code fix. No correctness, accessibility, or coverage defects found.

🏄 Total glassy set, dude — this Reveal wave rolls in clean: SSR-safe, reduced-motion-friendly, 100% covered, no wipeouts. Just one tiny ripple on the as prop worth a heads-up in the docs, then paddle it out to a human. Good to go, brah.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
bulma-ui/src/scss/components/_reveal.scss (1)

38-43: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

will-change is never reset after reveal.

will-change: opacity, transform is applied to the hidden state but not cleared in the .is-revealed final state, so browsers keep a compositing layer alive for every revealed element indefinitely. This is more impactful with cascade mode where many children can accumulate persistent layers.

♻️ Suggested fix: reset will-change once revealed
 .#{iv.$class-prefix}reveal-fade.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-fade-up.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-fade-down.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-slide-left.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-slide-right.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-zoom.#{iv.$class-prefix}is-revealed,
 .#{iv.$class-prefix}reveal-flip.#{iv.$class-prefix}is-revealed {
   opacity: 1;
   transform: none;
+  will-change: auto;
 }

Also applies to: 70-79

🤖 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 `@bulma-ui/src/scss/components/_reveal.scss` around lines 38 - 43,
`will-change: opacity, transform` is left on the hidden reveal state and never
cleared, so update the reveal styles in `_reveal.scss` to reset `will-change` in
the final `.is-revealed` state (and any cascade-related revealed child state)
instead of keeping it active after the transition. Use the existing reveal
selectors and mixin-generated rules for the hidden/revealed states to ensure the
compositing hint is only applied during animation.
🤖 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 `@bulma-ui/src/components/Reveal.tsx`:
- Line 46: RevealProps.threshold can currently pass invalid values into the
IntersectionObserver setup in Reveal, causing the effect to throw for NaN or
values outside 0–1. Update the observer initialization path in Reveal to
validate or clamp threshold before creating the observer, and fall back to a
safe default when the prop is invalid. Make sure the fix is applied wherever the
observer is constructed in the Reveal component logic.

---

Nitpick comments:
In `@bulma-ui/src/scss/components/_reveal.scss`:
- Around line 38-43: `will-change: opacity, transform` is left on the hidden
reveal state and never cleared, so update the reveal styles in `_reveal.scss` to
reset `will-change` in the final `.is-revealed` state (and any cascade-related
revealed child state) instead of keeping it active after the transition. Use the
existing reveal selectors and mixin-generated rules for the hidden/revealed
states to ensure the compositing hint is only applied during animation.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 83a563c8-9b70-4552-8e35-a320f9559a6f

📥 Commits

Reviewing files that changed from the base of the PR and between 2773f8f and 0aa5d6b.

📒 Files selected for processing (8)
  • bulma-ui/src/components/Reveal.stories.tsx
  • bulma-ui/src/components/Reveal.tsx
  • bulma-ui/src/components/__tests__/Reveal.test.tsx
  • bulma-ui/src/index.ts
  • bulma-ui/src/scss/components/_index.scss
  • bulma-ui/src/scss/components/_reveal.scss
  • docs/docs/api/components/reveal.md
  • skills/bestax-custom-component/references/component-catalog.md

Comment thread bulma-ui/src/components/Reveal.tsx
@github-actions

github-actions Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

AI loop status: iteration 1/4 — fix-reviews at 0aa5d6b (2026-07-07T11:33:02Z).
Managed by claude-pr-loop.yml. To resume after a pause: set iteration back to 0 on the first line, re-add ai-loop, remove ai-loop-paused, then re-run via workflow_dispatch.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

AI loop stopped — contested findings. The fix agent pushed no changes and 2 review thread(s) remain open (refuted or unaddressable). A human ruling is needed; see the fix agent's summary above.

@github-actions github-actions Bot added needs-human-review Loop converged (or contested): awaiting owner review + manual merge and removed ai-loop AI-authored PR in the autonomous review/fix loop labels Jul 7, 2026
@github-actions
github-actions Bot requested a review from allxsmith July 7, 2026 06:36
allxsmith added a commit that referenced this pull request Jul 7, 2026
…recipe (#248)

On PR #247 the fix agent ran clean (opus, 24 turns, success) but hit 15
permission denials and produced ZERO output — no commits, no thread replies,
no recap — so the loop escalated with the findings unaddressed. Same class
as the original #238 thrash: the agent reached for a tool that is not
allowlisted and gave up. The execution-output artifact that would name the
tool is defeated by the proxy blocking its blob-storage host, and the job
log hides per-turn detail by default.

Two fixes:
- show_full_output: true on the fix AND verify jobs, so every tool call and
  denial lands in the readable job log (no artifact needed).
- Give both agents the explicit gh-api thread-reply recipe
  (addPullRequestReviewThreadReply) and state plainly that there is NO MCP
  'reply' tool here — only mcp__github_inline_comment__create_inline_comment
  for a NEW comment. Reaching for a nonexistent MCP reply tool is the most
  likely denial source; this makes the reply path deterministic.


Claude-Session: https://claude.ai/code/session_01NVR5yWevceZEFbTJmpviiM

Co-authored-by: Claude <noreply@anthropic.com>
@allxsmith allxsmith added ai-loop AI-authored PR in the autonomous review/fix loop and removed needs-human-review Loop converged (or contested): awaiting owner review + manual merge labels Jul 7, 2026 — with Claude
…stribution

Clamp the threshold prop into the 0-1 range and fall back to the 0.15 default
for non-finite values before handing it to IntersectionObserver, which
otherwise throws a RangeError for NaN or out-of-range thresholds. Adds tests
for the clamp-high, clamp-low, and non-finite fallback paths (Reveal.tsx stays
at 100% coverage).

Also documents, on the as-prop JSDoc and the docs page, where a caller's
className/style/helper classes vs. id/aria/data/event-handler props land when
as is a component (observed wrapper div) vs. a plain intrinsic tag (single
element).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bestaxbot

Copy link
Copy Markdown
Collaborator Author

Fix pass recap (iteration 1)

Both open AI review threads were legitimate and in scope, so both are addressed in eb781a7.

Fixed

Finding Thread What changed
IntersectionObserver throws a RangeError for NaN/out-of-range threshold CodeRabbit (r3534155432) Clamp threshold to 0–1 and fall back to 0.15 for non-finite values before building the observer; added clamp-high / clamp-low / non-finite tests. Reveal.tsx stays at 100% coverage.
as-as-component splits props across two elements (undocumented) Claude deep review (r3534149796) Documented the asymmetry on the as prop JSDoc and added a :::note to the docs page (including the #hero.highlight combined-selector gotcha). Runtime behavior intentionally unchanged.

Refuted

None — nothing this pass was out of scope or stale.

Gates run

  • tsc --noEmit (bulma-ui) — clean
  • eslint src (bulma-ui) — 0 errors (only pre-existing warnings in Autocomplete/Slider/Taginput)
  • jest Reveal.test.tsx with coverage — 26 tests pass, Reveal.tsx 100% stmts/branch/funcs/lines
  • prettier --check — all files formatted
  • gen:catalog:check — catalog not stale

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://521535d0.bestax.pages.dev

allxsmith added a commit that referenced this pull request Jul 7, 2026
* ci: log full fix/verify output + tell agents the gh-api thread-reply recipe

On PR #247 the fix agent ran clean (opus, 24 turns, success) but hit 15
permission denials and produced ZERO output — no commits, no thread replies,
no recap — so the loop escalated with the findings unaddressed. Same class
as the original #238 thrash: the agent reached for a tool that is not
allowlisted and gave up. The execution-output artifact that would name the
tool is defeated by the proxy blocking its blob-storage host, and the job
log hides per-turn detail by default.

Two fixes:
- show_full_output: true on the fix AND verify jobs, so every tool call and
  denial lands in the readable job log (no artifact needed).
- Give both agents the explicit gh-api thread-reply recipe
  (addPullRequestReviewThreadReply) and state plainly that there is NO MCP
  'reply' tool here — only mcp__github_inline_comment__create_inline_comment
  for a NEW comment. Reaching for a nonexistent MCP reply tool is the most
  likely denial source; this makes the reply path deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVR5yWevceZEFbTJmpviiM

* ci: bestaxbot signs off the terminal states in surfer dialect

The loop's human-facing terminal comments (converged / contested /
paused / wipeout) were posted by github-actions[bot] in flat prose.
Give them a personality: bestaxbot now authors them (via AI_LOOP_PAT;
the label + reviewer edits stay on the job token), in a
Californian/Hawaiian surfer voice with an AI-superiority streak —
'clean set, total convergence, need a meat sack to hit merge.'

Covers all five terminal comments: converged handoff, contested
findings, halt (cap/protected/review-failed/cr-stalled), fix-run
wipeout, and verify no-progress. Only the comment author + wording
change; labels, reviewers, and the machine-parsed ai-loop-state
comment are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVR5yWevceZEFbTJmpviiM

---------

Co-authored-by: Claude <noreply@anthropic.com>
@allxsmith allxsmith closed this Jul 7, 2026
allxsmith added a commit that referenced this pull request Jul 7, 2026
… MCP (#250)

Implement run #18 fully built the Reveal component (gates green, 100%
coverage) but produced no branch/PR: it reached for
mcp__github_file_ops__commit_files, which is not allowlisted, and gave up
blocked — even though the git-CLI commit/push verbs ARE allowlisted and are
what runs #16/#17 used to open #243 and #247. Non-deterministic tool choice,
same class as the fixer reaching for a nonexistent MCP reply tool.

- Rewrite step 4 with an explicit git-CLI commit+push recipe
  (git checkout -b / add / commit / push) and state plainly that there is NO
  MCP commit tool here — git is already set up to SSH-sign as bestaxbot, so
  git-CLI commits come out Verified.
- Add show_full_output so a blocked commit is visible in the log, not just an
  artifact the proxy won't let us download.


Claude-Session: https://claude.ai/code/session_01NVR5yWevceZEFbTJmpviiM

Co-authored-by: Claude <noreply@anthropic.com>
@allxsmith
allxsmith deleted the claude/issue-197-20260707-0617 branch July 31, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-loop AI-authored PR in the autonomous review/fix loop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Component Proposal] Reveal / ScrollAnimation — animate sections into view on scroll

2 participants