Skip to content

feat(dashboard): animate value figures with slot-text, drop dead odometer#39

Merged
finallyjay merged 4 commits into
mainfrom
feat/slot-text-animated-values
Jul 8, 2026
Merged

feat(dashboard): animate value figures with slot-text, drop dead odometer#39
finallyjay merged 4 commits into
mainfrom
feat/slot-text-animated-values

Conversation

@finallyjay

Copy link
Copy Markdown
Owner

Closes #38

What

  • Adds slot-text (pinned 0.3.2, dependency-free, ~2KB gzip, MIT) and a thin components/ui/animated-text.tsx wrapper — the single tuning point for roll options, plus a prefers-reduced-motion fallback (plain <span>) that the library doesn't provide.
  • Wires it into the two dashboard values that change in-place: the headline "Inventory value" figure (rolls after Sync now) and the selected-day holdings total (rolls when stepping days with ‹ › or clicking the chart). Animates the full formatPrice() output — symbol, separators and decimals included.
  • Deletes the hand-rolled animated-number.tsx odometer and completion-ring.tsx: both dead code from the base project (the ring was never rendered anywhere, and it was the odometer's only importer). Updates the UI-kit list in CLAUDE.md.

Why slot-text over the hand-rolled odometer

The odometer only handled ungrouped integers (no symbol/decimals), always rolled from 0 on mount, and was 113 lines of unused maintenance surface. slot-text animates any short string per-character; its one caveat (kerning/ligature loss) is moot with the theme's monospace fonts.

Tests

test/animated-text.test.tsx (jsdom): renders through slot-text with the text as accessible name (aria-label), falls back to a plain span under reduced motion, and updates the accessible name on prop change. pnpm lint, full suite (85 tests) and pnpm build all green.

Note: not verified visually in a browser — the dashboard is behind Steam auth; verify with pnpm dev → sync / step days.

🤖 Generated with Claude Code

…eter

Adopt the slot-text library (pinned 0.3.2, dependency-free, ~2KB) to roll
the dashboard headline inventory value and the selected-day holdings total
whenever they change (sync, day stepping, chart clicks). The new
components/ui/animated-text.tsx wrapper is the single tuning point and adds
a prefers-reduced-motion fallback the library lacks.

Delete the hand-rolled animated-number.tsx odometer and completion-ring.tsx:
both were dead code inherited from the base project (the ring was never
rendered, and it was the odometer's only consumer).

Closes #38

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 8, 2026 11:49

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added smooth animated value displays across inventory and dashboard totals, with support for reduced-motion preferences.
    • Introduced improved motion-friendly text updates for key metrics and prices.
  • Bug Fixes

    • Prevented distracting chart redraws during refreshes, making sync updates feel smoother.
    • Kept inventory lists visible during background refreshes so price changes update in place without flicker.
  • Tests

    • Added coverage for animated text behavior and reduced-motion handling.

Walkthrough

Adds AnimatedText with reduced-motion fallback, applies it to dashboard and inventory value displays, changes sync refresh behavior to keep the list mounted, and updates docs/tests.

Changes

Animated Text Adoption

Layer / File(s) Summary
AnimatedText component
components/ui/animated-text.tsx, package.json, CLAUDE.md, test/animated-text.test.tsx
Adds the AnimatedText client component, reduced-motion handling, the slot-text dependency, usage notes, and tests for motion-allowed, reduced-motion, and update behavior.
Value display adoption
components/dashboard/inventory-value-panel.tsx, components/inventory/holding-row.tsx, components/inventory/inventory-list.tsx, components/inventory/item-price-chart.tsx
Replaces static formatted prices with AnimatedText across dashboard totals, holding rows, inventory item pricing, and chart stat values.
Inventory sync refresh
components/inventory/inventory-list.tsx
Adds background refresh behavior for sync, keeping the list visible while refreshes run and changing failure handling for that path.

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

Possibly related PRs

Poem

A bunny taps the stocky keys,
And rolls the numbers with the breeze.
The values hop, the list stays still,
Reduced-motion rests, as rabbits will.
🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the dashboard animation work and dead-code cleanup.
Description check ✅ Passed The description covers the why, what, tests, and linked issue, though it does not follow the template headings exactly.
Linked Issues check ✅ Passed The PR implements #38 by animating the two dashboard values, adding reduced-motion fallback, and deleting the dead components.
Out of Scope Changes check ✅ Passed The added tests, dependency, docs, and loading tweaks are all directly tied to the stated animation refactor.
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 feat/slot-text-animated-values

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

@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

🤖 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 `@package.json`:
- Line 55: The dependency version for slot-text is pointing at a non-existent
release, which will break installation in CI. Update the slot-text entry in
package.json to a published npm version that actually exists, and keep the
dependency name unchanged so pnpm can resolve it successfully.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78a59a31-da79-4c73-b84c-141ce1749962

📥 Commits

Reviewing files that changed from the base of the PR and between cf06c13 and b81658a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • CLAUDE.md
  • components/dashboard/inventory-value-panel.tsx
  • components/ui/animated-number.tsx
  • components/ui/animated-text.tsx
  • components/ui/completion-ring.tsx
  • package.json
  • test/animated-text.test.tsx
💤 Files with no reviewable changes (2)
  • components/ui/animated-number.tsx
  • components/ui/completion-ring.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
components/ui/**

📄 CodeRabbit inference engine (CLAUDE.md)

Use the shared shadcn/ui primitive components and related UI utilities from the components/ui/ directory for reusable interface elements.

Files:

  • components/ui/animated-text.tsx
components/dashboard/inventory-value-panel.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Show the latest inventory value and a Recharts history line, with a manual “Sync now” button.

Files:

  • components/dashboard/inventory-value-panel.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker

Timestamp: 2026-07-08T11:50:14.016Z
Learning: Use the provided pnpm commands for development, build, linting, type-checking, testing, formatting, and running single Vitest files.
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker

Timestamp: 2026-07-08T11:50:14.016Z
Learning: CI runs install → lint → test → build, and pre-commit hooks run oxfmt + oxlint via Husky and lint-staged.
Learnt from: CR
Repo: finallyjay/csgo-inventory-tracker

Timestamp: 2026-07-08T11:50:14.016Z
Learning: The project is a Next.js 16 App Router application using React 19, TypeScript strict mode, Tailwind CSS 4, and shadcn/ui + Radix UI primitives.
🔇 Additional comments (5)
components/ui/animated-text.tsx (2)

17-25: SSR-safe reduced-motion detection.

useSyncExternalStore with a getServerSnapshot of () => false correctly avoids calling window.matchMedia during SSR/hydration. No issues here.


37-45: LGTM!

components/dashboard/inventory-value-panel.tsx (1)

6-6: LGTM!

Also applies to: 129-131, 245-247

test/animated-text.test.tsx (1)

1-47: LGTM!

CLAUDE.md (1)

160-167: LGTM!

Comment thread package.json
…feel

The inventory list swapped the whole grid for skeletons on every reload,
so nothing could animate after a sync — refetch in the background instead,
keeping the cards mounted while their prices roll (list total, line totals
and per-unit prices now use AnimatedText).

On the dashboard, every post-sync refetch handed recharts a fresh data
array and the line replayed its full draw animation, which read as the
entire panel reloading. Disable the line animation; the rolling value is
the sync feedback now.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/inventory/inventory-list.tsx (1)

120-139: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Background refresh failure still wipes the mounted list.

background only gates setLoading(true). On the !res.ok branch and in catch, setData(null)/setError(...) still run unconditionally, flipping the render to the error branch (Line 245) and unmounting the list — exactly the skeleton-swap/mount-churn this change is meant to avoid on a sync-triggered background refresh.

🐛 Proposed fix to keep the list mounted on background failures
   const load = useCallback(async (background = false) => {
     if (!background) setLoading(true)
-    setError(null)
+    if (!background) setError(null)
     try {
       const res = await fetch("/api/inventory/items", { cache: "no-store" })
       const body = await res.json()
       if (!res.ok) {
-        setError(body.error ?? "Failed to load inventory.")
-        setData(null)
+        if (background) {
+          toast({ title: "Refresh failed", description: body.error ?? "Unknown error", variant: "destructive" })
+        } else {
+          setError(body.error ?? "Failed to load inventory.")
+          setData(null)
+        }
         return
       }
       setData(body as InventoryItemsResponse)
     } catch {
-      setError("Network error while loading inventory.")
+      if (background) {
+        toast({ title: "Network error", variant: "destructive" })
+      } else {
+        setError("Network error while loading inventory.")
+      }
     } finally {
       setLoading(false)
     }
   }, [])

Also applies to: 155-155

🤖 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 `@components/inventory/inventory-list.tsx` around lines 120 - 139, The `load`
callback in `inventory-list.tsx` still clears `data` and sets an error on failed
requests even when `background` is true, which causes the mounted list to
unrender during background refreshes. Update `load` so the `!res.ok` and `catch`
paths preserve the current `data` when `background` is true, and only call
`setError`/`setData(null)` for foreground loads; keep `setLoading(false)` in
`finally` as-is. Use the `load` callback and the background refresh flow to
locate the change.
🤖 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.

Outside diff comments:
In `@components/inventory/inventory-list.tsx`:
- Around line 120-139: The `load` callback in `inventory-list.tsx` still clears
`data` and sets an error on failed requests even when `background` is true,
which causes the mounted list to unrender during background refreshes. Update
`load` so the `!res.ok` and `catch` paths preserve the current `data` when
`background` is true, and only call `setError`/`setData(null)` for foreground
loads; keep `setLoading(false)` in `finally` as-is. Use the `load` callback and
the background refresh flow to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 414eb2fb-7a41-4750-acff-82a9111b7070

📥 Commits

Reviewing files that changed from the base of the PR and between b81658a and e790aa5.

📒 Files selected for processing (2)
  • components/dashboard/inventory-value-panel.tsx
  • components/inventory/inventory-list.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Verify the session cookie signature, re-check whitelist access on every auth lookup, and derive isAdmin from ADMIN_STEAM_ID rather than storing it in the cookie.

Files:

  • components/dashboard/inventory-value-panel.tsx
  • components/inventory/inventory-list.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use the design tokens defined by the project theme instead of hardcoded white-based utility colors when styling React components.

Files:

  • components/dashboard/inventory-value-panel.tsx
  • components/inventory/inventory-list.tsx
**/components/dashboard/inventory-value-panel.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Include the latest inventory value, the history line chart, and the manual "Sync now" action in the dashboard value panel.

Files:

  • components/dashboard/inventory-value-panel.tsx
🔇 Additional comments (3)
components/dashboard/inventory-value-panel.tsx (2)

6-6: LGTM!

Also applies to: 129-131, 249-251


187-190: LGTM!

components/inventory/inventory-list.tsx (1)

6-6: LGTM!

Also applies to: 80-86, 179-181

finallyjay and others added 2 commits July 8, 2026 14:01
AnimatedText now masks every digit to 0 on first paint and rolls the real
digits in right after mount, so the animation plays on every page load and
remount — not only when a value changes later. Width stays stable (same
character count) and the accessible name is pinned to the real value via
aria-label while the masked frame is up.

Extend the roll to the remaining price figures: holdings rows (line total
and per-unit, also rolling in place when stepping days thanks to their
stable keys) and the Latest/Low/High stats on the item detail page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed post-sync refetch went through the same setData(null)/setError
path as the initial load, wiping the list the background mode was meant
to keep on screen. Toast the failure instead and leave the data as-is;
the initial-load error UI is unchanged.

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

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

🧹 Nitpick comments (1)
components/inventory/holding-row.tsx (1)

43-49: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider mount-triggered animation cost at scale.

Since AnimatedText masks digits on mount and rolls in via useEffect on every mount, every HoldingRow in a large inventory list will animate simultaneously whenever the list (re)mounts (initial load, filter/sort changes causing key changes, etc.), not just on real price updates. Worth verifying this doesn't produce a jarring "cascade" effect or performance hit for large inventories.

🤖 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 `@components/inventory/holding-row.tsx` around lines 43 - 49, The issue is that
`AnimatedText` in `HoldingRow` will re-run its mount animation for every row
whenever the list remounts or keys change, which can cause a cascade effect and
extra work in large inventories. Update `HoldingRow` and/or `AnimatedText` so
the animation is only triggered when the displayed price value actually changes,
not on every mount; use the existing `AnimatedText` usage around
`formatPrice(item.lineTotal ?? 0, currency)` and `formatPrice(item.unitPrice,
currency)` to locate the affected render paths.
🤖 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.

Nitpick comments:
In `@components/inventory/holding-row.tsx`:
- Around line 43-49: The issue is that `AnimatedText` in `HoldingRow` will
re-run its mount animation for every row whenever the list remounts or keys
change, which can cause a cascade effect and extra work in large inventories.
Update `HoldingRow` and/or `AnimatedText` so the animation is only triggered
when the displayed price value actually changes, not on every mount; use the
existing `AnimatedText` usage around `formatPrice(item.lineTotal ?? 0,
currency)` and `formatPrice(item.unitPrice, currency)` to locate the affected
render paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e180f9f8-59fe-4f69-b8fe-8e8769a27bae

📥 Commits

Reviewing files that changed from the base of the PR and between e790aa5 and 5b48543.

📒 Files selected for processing (4)
  • components/inventory/holding-row.tsx
  • components/inventory/inventory-list.tsx
  • components/inventory/item-price-chart.tsx
  • components/ui/animated-text.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/ui/animated-text.tsx
  • components/inventory/inventory-list.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use the project’s UTC date convention: persist timestamps and snapshot days in UTC, treat formatDay() as a UTC bucket key that must never timezone-shift, and use formatDateTime() only for real timestamps displayed in the viewer’s timezone.
Store and compute money as integer minor units everywhere (never floats) to avoid precision drift.
Use the repository’s authenticated-session conventions: verify the signed steam_user cookie, re-check whitelist access on each auth-gated request, and derive admin status from ADMIN_STEAM_ID rather than storing it in the cookie.
Prefer client-safe, pure helpers for shared utility modules when possible, and keep server-only behavior isolated to server modules.
Use @/* as the project-root path alias for imports.

Files:

  • components/inventory/holding-row.tsx
  • components/inventory/item-price-chart.tsx
🔇 Additional comments (2)
components/inventory/holding-row.tsx (1)

4-4: LGTM!

Also applies to: 43-49

components/inventory/item-price-chart.tsx (1)

6-6: LGTM!

Also applies to: 38-40

@finallyjay finallyjay merged commit b0f04bd into main Jul 8, 2026
5 checks passed
@finallyjay finallyjay deleted the feat/slot-text-animated-values branch July 8, 2026 12:10
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.

Animate dashboard values with slot-text

2 participants