AnimatedBar

PURPOSE

A single WAAPI-driven progress bar for the resolve phase. Animates a fill from a before state to an after state across one or more level boundaries, fires a one-shot onComplete when the visual is done, and supports a skip path that jumps straight to the final visual.

OWNS

  • The <div> track + fill + counter DOM nodes (refs fillRef, counterRef).
  • A completedRef boolean guard ensuring onComplete fires exactly once per mount/effect run.
  • Inline style objects: containerStyle, labelStyle, trackStyle, counterStyle, and the fillStyle(color) factory.
  • The decision of where the initial scaleX sits on first render (derived from delta.before + getXpForLevel).

READS FROM

  • props.delta: ProgressDeltabefore/after XP+level pair, optional displayLabel, and optional thresholdsCrossed flag. Imported type from ../data/reward-types.
  • props.getXpForLevel(level) — caller-supplied curve lookup; used both for initial percent and for the final skip jump. Multi-segment fills inside fillBar also consume it via the options bag.
  • props.color — CSS color string for the fill. Defaults to '#4fc3f7'.
  • props.skip — boolean; when true, bypasses the WAAPI animation entirely.

PUSHES TO

  • The fill DOM node’s style.transform (via scaleX) on the skip path.
  • The counter DOM node’s textContent on the skip path.
  • fillBar(...) from ../fx/juice — delegated all real animation work (multi-segment fills, head-flare, overshoot, counter roll, threshold pauses).
  • props.onComplete() — exactly once, gated by completedRef.

DOES NOT

  • Read or write any Zustand store. Source of truth is the ProgressDelta prop, never live state.
  • Mutate delta or any prop.
  • Manage its own lifecycle beyond a single useEffect keyed on [delta, skip].
  • Handle taps/clicks directly. Skip semantics are controlled by the parent flipping skip to true.
  • Decide pacing or sequencing between multiple bars. The parent (HomeResolveController) chains bars by waiting on onComplete.
  • Read level curves itself. All curve evaluation is delegated to getXpForLevel.

Signals

  • onComplete: () => void — fired exactly once per effect run when either the WAAPI animation resolves or the skip jump finishes. Guarded by completedRef so it cannot double-fire.
  • Threshold callback passed into fillBar is a no-op (void id) when delta.thresholdsCrossed is truthy — fillBar owns the pause-and-pop behavior; this component only opts in.

Entry points

  • export interface AnimatedBarProps — public prop shape.
  • export function AnimatedBar(props) — the component itself; no default export.

Pattern notes

  • Imperative ref + WAAPI, not React state. All animation lives on DOM nodes via fillBar; React only renders the initial frame. This avoids per-frame re-renders during the fill.
  • Initial render matches delta.before exactly. The first paint sets scaleX(initialPct) and counter text to before.xp / xpForLevel(before.level) so there is no flash before the effect runs.
  • Effect deps disable the exhaustive-deps lint intentionally on the [delta, skip] dep set — getXpForLevel and onComplete are treated as stable for the bar’s lifetime; the parent is expected to keep references stable across renders of the same bar.
  • One-shot completion guard. completedRef survives StrictMode double-invocation and any race between the skip path and a resolving fillBar promise.
  • Defensive percent clamp. Math.min(1, pct) on both initial and skip-final transforms tolerates after.xp > xpForLevel(after.level) without overflowing the track visually.
  • No-op early return when fillRef.current is null still calls finish() so a parent waiting on onComplete cannot stall if the node failed to mount.

EXTRACT-CANDIDATE

  • The pattern “compute fill percent as xp / xpForLevel(level) with a divide-by-zero guard and Math.min(1, ...) clamp” appears twice in this file and is the same arithmetic fillBar performs internally. If a third caller appears, lift to a shared xpToPct(xp, level, getXpForLevel) helper next to ProgressDelta in data/reward-types or in fx/juice.
  • The “fire onComplete exactly once across skip + async paths” idiom (ref-guarded finish) is likely to recur in any resolve-phase animation component. Candidate for a tiny useOnce(cb) hook in metagame/hooks/ if a second user shows up.