Skip to content

feat(ui): animate loading bar with traveling wave - #332

Merged
bordalix merged 3 commits into
masterfrom
animate-loading-bar
Feb 26, 2026
Merged

bordalix merged 3 commits into
masterfrom
animate-loading-bar

Conversation

@sahilc0

@sahilc0 sahilc0 commented Feb 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replace the static pixel-art battery SVG with a smooth traveling wave animation
  • Each of the 6 segments oscillates between 30-100% fill height using eased sine waves with phase offsets, creating a fluid wave effect
  • Uses requestAnimationFrame + refs for 60fps animation without React re-renders per frame
  • Respects prefers-reduced-motion via framer-motion's useReducedMotion hook
  • Add cloudflare tunnel host to vite allowedHosts for mobile HTTPS testing

Details

The animation applies ease-in-out-cubic easing to a raw sine wave for each segment, with staggered phase offsets (0.45 per segment) creating the traveling wave. Shadow opacity tracks fill level for depth. No new dependencies — uses existing framer-motion for reduced-motion detection only.

Test plan

  • pnpm start — loading screen shows animated battery wave
  • Trigger loading states (app init, unlock, send) — animation plays smoothly
  • Toggle reduced motion in OS settings — animation stops, static full battery shown
  • No console errors, smooth 60fps in DevTools Performance tab
  • Test on mobile via cloudflare tunnel HTTPS URL

Files changed

File Change
src/icons/LoadingBar.tsx Rewritten with animated traveling wave
vite.config.ts Added .trycloudflare.com to allowedHosts
src/index.css Whitespace cleanup
src/index.tsx Whitespace cleanup

Summary by CodeRabbit

  • New Features

    • Loading bar now features dynamic, animated segments that update in real time and respect reduced-motion preferences.
  • Chores

    • Minor formatting and development configuration updates with no change to user-facing behavior.

Replace the static pixel-art battery SVG with a smooth traveling wave
animation. Each of the 6 segments oscillates between 30-100% fill height
using eased sine waves with phase offsets, creating a fluid wave effect.

- Use requestAnimationFrame + refs for 60fps without React re-renders
- Apply ease-in-out-cubic to raw sine for natural motion
- Respect prefers-reduced-motion via useReducedMotion hook
- Add cloudflare tunnel host to vite allowedHosts for mobile testing

Files: src/icons/LoadingBar.tsx, vite.config.ts
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Feb 25, 2026 •

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: a22fc35
Status: ✅  Deploy successful!
Preview URL: https://2192e9f6.wallet-bitcoin.pages.dev
Branch Preview URL: https://animate-loading-bar.wallet-bitcoin.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Feb 25, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a data-driven, animated LoadingBar React SVG component using per-segment RAF animation with reduced-motion support; updates Vite dev server to allow hosts matching .trycloudflare.com; includes a minor formatting change in src/index.tsx.

Changes

Cohort / File(s) Summary
Animated LoadingBar component
src/icons/LoadingBar.tsx
Introduces a refactored, data-driven SVG loading bar: SEGMENTS data, color/layout constants, easing helpers (easedSine, easeInOutCubic), per-segment refs, RAF-based animation loop, and useReducedMotion support. Replaces many hard-coded rects with mapped segment rendering and reorganizes SVG structure (terminals, borders, dividers, corners vs animated bodies/shadows).
Dev server config
vite.config.ts
Adds allowedHosts: ['.trycloudflare.com'] to Vite dev server configuration.
Formatting
src/index.tsx
Minor whitespace/formatting change (blank line added); no logic changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • pietro909
  • bordalix
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: animating the loading bar with a traveling wave effect, which is the primary feature introduced in LoadingBar.tsx.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch animate-loading-bar

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 and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Feb 25, 2026 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: a22fc35
Status: ✅  Deploy successful!
Preview URL: https://b83d789d.arkade-wallet.pages.dev
Branch Preview URL: https://animate-loading-bar.arkade-wallet.pages.dev

View logs

… hook

framer-motion is not a project dependency. Replace the import with a
lightweight inline hook using window.matchMedia to detect the
prefers-reduced-motion preference.

@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

🧹 Nitpick comments (2)
src/icons/LoadingBar.tsx (1)

45-45: Use SEGMENTS.length instead of hard-coded 6.

Line 45 should derive loop bounds from the data source to prevent drift during future edits.

♻️ Proposed change
-      for (let i = 0; i < 6; i++) {
+      for (let i = 0; i < SEGMENTS.length; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/LoadingBar.tsx` at line 45, The loop in LoadingBar that currently
uses a hard-coded bound (for (let i = 0; i < 6; i++)) should use the SEGMENTS
data source length to avoid drift; update the loop to iterate up to
SEGMENTS.length (reference the SEGMENTS constant and the loop inside the
LoadingBar component) so additions/removals to SEGMENTS automatically adjust the
loop count.
vite.config.ts (1)

20-20: Scope tunnel host allowlisting to explicit dev sessions.

Line 20 permanently allows any *.trycloudflare.com host while line 19 exposes the dev server on all interfaces. Consider env-scoping the tunnel host allowlist to make it opt-in rather than always active, following the pattern used on line 15 with process.env.HTTPS.

♻️ Suggested refactor
   server: {
     port: 3002,
     host: true,
-    allowedHosts: ['.trycloudflare.com'],
+    allowedHosts: process.env.CF_TUNNEL_HOST ? [process.env.CF_TUNNEL_HOST] : [],
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vite.config.ts` at line 20, Scope the hardcoded allowedHosts entry so the
trycloudflare tunnel is opt-in: instead of always setting allowedHosts:
['.trycloudflare.com'], guard it behind an environment check (similar to the
existing process.env.HTTPS pattern) and only return the array when the env var
(e.g. process.env.USE_TUNNEL or process.env.DEV_TUNNEL) is truthy; otherwise
omit or set allowedHosts to undefined/[] — also ensure this check pairs with the
dev server host setting (server.host) that exposes all interfaces so the tunnel
is only permitted for explicit dev sessions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/icons/LoadingBar.tsx`:
- Around line 39-42: When shouldReduce flips to true inside the LoadingBar
animation block, the current code cancels the RAF but leaves segment
heights/opacities mid-animation; update the if (shouldReduce) branch in
LoadingBar.tsx to (1) cancelAnimationFrame(rafRef.current), (2) clear
rafRef.current, and (3) reset all battery segment elements/state (e.g., via
existing segment refs or a helper like resetSegmentsToFullBattery) to full
height and full opacity so the static full-battery appearance is restored.
Ensure this runs synchronously before returning so no partial-filled visuals
remain.
- Around line 1-2: The file imports useReducedMotion from 'framer-motion' which
causes a CI unresolved-import; remove that external dependency by replacing the
framer-motion call with a small native hook (e.g., implement
usePrefersReducedMotion using window.matchMedia('(prefers-reduced-motion:
reduce)') and subscribing to its change events) and use that hook in the
LoadingBar component instead of useReducedMotion; update any references to
useReducedMotion in this file (and keep existing useEffect/useRef usage) so
behavior is identical while avoiding adding framer-motion to package.json.

---

Nitpick comments:
In `@src/icons/LoadingBar.tsx`:
- Line 45: The loop in LoadingBar that currently uses a hard-coded bound (for
(let i = 0; i < 6; i++)) should use the SEGMENTS data source length to avoid
drift; update the loop to iterate up to SEGMENTS.length (reference the SEGMENTS
constant and the loop inside the LoadingBar component) so additions/removals to
SEGMENTS automatically adjust the loop count.

In `@vite.config.ts`:
- Line 20: Scope the hardcoded allowedHosts entry so the trycloudflare tunnel is
opt-in: instead of always setting allowedHosts: ['.trycloudflare.com'], guard it
behind an environment check (similar to the existing process.env.HTTPS pattern)
and only return the array when the env var (e.g. process.env.USE_TUNNEL or
process.env.DEV_TUNNEL) is truthy; otherwise omit or set allowedHosts to
undefined/[] — also ensure this check pairs with the dev server host setting
(server.host) that exposes all interfaces so the tunnel is only permitted for
explicit dev sessions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 374b68a and 5819259.

📒 Files selected for processing (3)
  • src/icons/LoadingBar.tsx
  • src/index.tsx
  • vite.config.ts

Comment thread src/icons/LoadingBar.tsx Resolved
Comment thread src/icons/LoadingBar.tsx
Comment on lines +39 to +42
if (shouldReduce) {
cancelAnimationFrame(rafRef.current)
return
}

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.

⚠️ Potential issue | 🟠 Major

Reduced-motion toggle can leave a partially filled battery.

When shouldReduce becomes true after animation has started, the loop stops but current segment heights/opacities are not restored. This breaks the “static full battery” behavior.

✅ Proposed fix
   useEffect(() => {
     if (shouldReduce) {
       cancelAnimationFrame(rafRef.current)
+      for (let i = 0; i < SEGMENTS.length; i++) {
+        const bodyEl = rectsRef.current[i]
+        const shadowEl = shadowsRef.current[i]
+        if (!bodyEl) continue
+        bodyEl.setAttribute('height', String(BODY_H))
+        bodyEl.setAttribute('y', String(BODY_Y))
+        if (shadowEl) shadowEl.style.opacity = '1'
+      }
       return
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (shouldReduce) {
cancelAnimationFrame(rafRef.current)
return
}
if (shouldReduce) {
cancelAnimationFrame(rafRef.current)
for (let i = 0; i < SEGMENTS.length; i++) {
const bodyEl = rectsRef.current[i]
const shadowEl = shadowsRef.current[i]
if (!bodyEl) continue
bodyEl.setAttribute('height', String(BODY_H))
bodyEl.setAttribute('y', String(BODY_Y))
if (shadowEl) shadowEl.style.opacity = '1'
}
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/LoadingBar.tsx` around lines 39 - 42, When shouldReduce flips to
true inside the LoadingBar animation block, the current code cancels the RAF but
leaves segment heights/opacities mid-animation; update the if (shouldReduce)
branch in LoadingBar.tsx to (1) cancelAnimationFrame(rafRef.current), (2) clear
rafRef.current, and (3) reset all battery segment elements/state (e.g., via
existing segment refs or a helper like resetSegmentsToFullBattery) to full
height and full opacity so the static full-battery appearance is restored.
Ensure this runs synchronously before returning so no partial-filled visuals
remain.

@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.

♻️ Duplicate comments (1)
src/icons/LoadingBar.tsx (1)

51-54: ⚠️ Potential issue | 🟠 Major

Reduced-motion toggle still leaves segments in a partial-fill state.

When shouldReduce flips to true mid-animation the RAF is cancelled but the body rects' height/y and shadows' opacity are left at whatever values the last frame wrote. The "static full battery" behaviour described in the PR is therefore broken whenever reduced motion is toggled on after the animation has started.

🛠️ Proposed fix — reset all segments synchronously before returning
  if (shouldReduce) {
    cancelAnimationFrame(rafRef.current)
+   for (let i = 0; i < SEGMENTS.length; i++) {
+     const bodyEl = rectsRef.current[i]
+     const shadowEl = shadowsRef.current[i]
+     if (!bodyEl) continue
+     bodyEl.setAttribute('height', String(BODY_H))
+     bodyEl.setAttribute('y', String(BODY_Y))
+     if (shadowEl) shadowEl.style.opacity = '1'
+   }
    return
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/LoadingBar.tsx` around lines 51 - 54, When shouldReduce becomes
true we currently cancel rafRef.current but leave SVG rects/shadows at mid-frame
values; update the shouldReduce branch inside the LoadingBar animation logic to
synchronously reset all segment elements to the "static full battery" state
before returning: cancel the RAF (rafRef.current), then iterate the same segment
collections used by the animator (e.g., bodyRectsRef / shadowRectsRef or the
arrays used in applyFrame/animateFrame) and set each body rect's height/y and
each shadow's opacity to the final static values the reduced-motion UI expects,
then return. Ensure you reference and mutate the same element refs/arrays used
by the animation so the DOM is deterministically reset when reduced motion is
toggled.
🧹 Nitpick comments (1)
src/icons/LoadingBar.tsx (1)

57-57: Use SEGMENTS.length instead of the magic number 6.

The loop bound is hardcoded to 6, which will silently go out of sync if the SEGMENTS array is ever extended or trimmed.

♻️ Proposed change
-     for (let i = 0; i < 6; i++) {
+     for (let i = 0; i < SEGMENTS.length; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/icons/LoadingBar.tsx` at line 57, Replace the hardcoded loop bound 6 with
SEGMENTS.length in the for loop inside LoadingBar.tsx so the iteration uses the
actual SEGMENTS array length; update the loop that currently reads "for (let i =
0; i < 6; i++)" to reference SEGMENTS.length to prevent it getting out of sync
when SEGMENTS is changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/icons/LoadingBar.tsx`:
- Around line 51-54: When shouldReduce becomes true we currently cancel
rafRef.current but leave SVG rects/shadows at mid-frame values; update the
shouldReduce branch inside the LoadingBar animation logic to synchronously reset
all segment elements to the "static full battery" state before returning: cancel
the RAF (rafRef.current), then iterate the same segment collections used by the
animator (e.g., bodyRectsRef / shadowRectsRef or the arrays used in
applyFrame/animateFrame) and set each body rect's height/y and each shadow's
opacity to the final static values the reduced-motion UI expects, then return.
Ensure you reference and mutate the same element refs/arrays used by the
animation so the DOM is deterministically reset when reduced motion is toggled.

---

Nitpick comments:
In `@src/icons/LoadingBar.tsx`:
- Line 57: Replace the hardcoded loop bound 6 with SEGMENTS.length in the for
loop inside LoadingBar.tsx so the iteration uses the actual SEGMENTS array
length; update the loop that currently reads "for (let i = 0; i < 6; i++)" to
reference SEGMENTS.length to prevent it getting out of sync when SEGMENTS is
changed.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5819259 and a22fc35.

📒 Files selected for processing (1)
  • src/icons/LoadingBar.tsx

@bordalix
bordalix merged commit c8ea13a into master Feb 26, 2026
5 checks passed
@bordalix
bordalix deleted the animate-loading-bar branch May 18, 2026 16:16
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.

2 participants