Skip to content

fix(dashboard): unfreeze animations broken by transition tokens used as durations#1386

Merged
gsxdsm merged 2 commits into
mainfrom
gsxdsm/fix-spinners
Jun 4, 2026
Merged

fix(dashboard): unfreeze animations broken by transition tokens used as durations#1386
gsxdsm merged 2 commits into
mainfrom
gsxdsm/fix-spinners

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Dashboard spinners and pulse animations rendered completely frozen — the in-progress status dots stopped pulsing, the Nodes/Todo view loaders and mention-popup spinners sat motionless, and onboarding/setup entrance animations never played. They all animate again.

The cause was invisible in devtools: the transition tokens bundle a duration and an easing (--transition-slow: 0.3s ease), and 15 animation declarations reused them as bare durations. Substituting 0.3s ease into an animation shorthand that names its own easing produces two <easing-function> values — and per the custom-properties spec, that makes the declaration invalid at computed-value time, resolving the whole property to animation: none with no parse error and no console warning. calc(var(--transition-slow) * 4) fails the same way (you can't multiply 0.3s ease). This is also why the two prior spinner fixes (FN-5855, FN-5913) didn't catch it: they covered the .animate-spin utility, which uses a literal 1s and was never affected.

What changed

  • New duration-only tokens --duration-instant/fast/normal/slow; the --transition-* tokens are now derived from them (var(--duration-slow) ease) so the two sets can't drift apart. Existing transition: usages are unaffected.
  • All 15 broken animation declarations (14 CSS files) switched to the duration tokens, preserving their effective durations.
  • New repo-wide regression test (animation-duration-tokens.css.test.ts) sweeps every CSS file under app/ and fails on the three invalid shapes: transition token + explicit easing in an animation shorthand, calc() over a transition token, and a transition token in animation-duration. It flagged all 15 sites red before the fix.

Test plan

  • New test: red before fix (15 violations enumerated) → green after
  • Real-browser verification against the production build: .status-dot--connecting went from animationName: none / 0 running animations → status-dot-pulse, 0.3s, ease-in-out, running; the calc-based NodesView pattern resolves to 1.2s and runs; transition: color var(--transition-fast) still computes to 0.15s / ease
  • Suites: foundation-ui 476 ✓, components-a 768 ✓, components-b 864 ✓

Compound Engineering
Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed frozen animations throughout the dashboard. Loading spinners, pulse effects, and entry animations now display correctly across multiple dashboard components including modals, status indicators, and expanding sections.

…as durations

Spinners, status-dot pulses, and entrance animations across the dashboard
rendered frozen: transition tokens (--transition-slow: 0.3s ease) bundle a
duration AND an easing, and 15 animation declarations reused them as bare
durations. Substituting "0.3s ease" next to an explicit easing (linear,
ease-in-out, ease-out) — or inside calc() — makes the declaration invalid at
computed-value time, which per spec resolves the entire property to
animation: none with no console error.

- add duration-only tokens (--duration-instant/fast/normal/slow) and derive
  the --transition-* tokens from them so the two cannot drift
- switch all 15 broken animation declarations across 14 CSS files to the
  duration tokens, preserving effective durations
- add animation-duration-tokens.css.test.ts: sweeps every app CSS file and
  fails on transition-token-as-duration, calc() over a transition token, and
  transition token in animation-duration

Verified in a real browser against the production build: previously frozen
.status-dot--connecting and calc-based NodesView spinners now report running
animations; transition shorthands still resolve to "0.15s / ease".

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

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gsxdsm, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4ba13c6-ab3d-4526-b3d4-6fed4b066436

📥 Commits

Reviewing files that changed from the base of the PR and between 2d2024f and c4ecf37.

📒 Files selected for processing (2)
  • docs/dashboard-guide.md
  • docs/solutions/ui-bugs/css-animation-frozen-by-transition-token-shape-mismatch.md
📝 Walkthrough

Walkthrough

This PR fixes a CSS animation regression where transition tokens (duration + easing) were incorrectly used as animation durations. The change introduces duration-only tokens, redefines transition tokens from those, updates all component animations, and adds validation to prevent recurrence.

Changes

CSS Animation Token Refactor

Layer / File(s) Summary
Token Contract Definition
packages/dashboard/app/styles.css
New --duration-* tokens (instant, fast, normal, slow) defined as bare durations; --transition-* tokens redefined as var(--duration-*) + easing. Status dot animation updated to use the new --duration-slow contract.
Regression Test and Configuration
packages/dashboard/app/__tests__/animation-duration-tokens.css.test.ts, packages/dashboard/vitest.config.ts
Vitest suite validates token definitions, scans all dashboard CSS for invalid --transition-* usage in animation declarations, and includes regression test. Test added to vitest config.
Component Animation Updates
packages/dashboard/app/components/*.css
Thirteen component stylesheets (CustomProvidersSection, DbCorruptionBanner, DockerProvisioningStatus, FileMentionPopup, IssueMentionPopup, ListView, NodesView, OnboardingDisclosure, OnboardingResumeCard, SetupWizardModal, SystemStatsModal, TaskIdIntegrityBanner, TodoView) updated to use --duration-* tokens in animation declarations.
Release Documentation
.changeset/fix-frozen-dashboard-animations.md
Changeset describing the token separation fix, its purpose, and the regression test mechanism.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Duration and easing, once tangled as one,
Are separated at last—the animation's now fun!
With tokens so proper, no more frozen spinners,
The dashboard regains all its graceful beginners.

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: fixing frozen dashboard animations caused by incorrect token usage in CSS.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsxdsm/fix-spinners

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.

@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: 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 `@packages/dashboard/app/__tests__/animation-duration-tokens.css.test.ts`:
- Around line 42-67: The current per-line checks in the lines.forEach loop miss
multiline declarations; update the test's findViolations logic to accumulate CSS
declarations across lines (e.g., join subsequent lines until a terminating ';'
or '}' or run the regexes against the full file/content chunk) before applying
the three detection rules (calc\(\s*var\(--transition-..., animation shorthand
check that uses EASING_KEYWORD, and animation-duration pattern). Modify the loop
that uses lines.forEach (or replace it) so animation/animation-duration/calc
patterns are evaluated on the assembled declaration string (referencing the
existing animationMatch handling and EASING_KEYWORD) to ensure multiline
declarations are detected.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3447d76-ee67-4b18-ac4e-6e8ff7f2efdb

📥 Commits

Reviewing files that changed from the base of the PR and between 07dd24e and 2d2024f.

📒 Files selected for processing (17)
  • .changeset/fix-frozen-dashboard-animations.md
  • packages/dashboard/app/__tests__/animation-duration-tokens.css.test.ts
  • packages/dashboard/app/components/CustomProvidersSection.css
  • packages/dashboard/app/components/DbCorruptionBanner.css
  • packages/dashboard/app/components/DockerProvisioningStatus.css
  • packages/dashboard/app/components/FileMentionPopup.css
  • packages/dashboard/app/components/IssueMentionPopup.css
  • packages/dashboard/app/components/ListView.css
  • packages/dashboard/app/components/NodesView.css
  • packages/dashboard/app/components/OnboardingDisclosure.css
  • packages/dashboard/app/components/OnboardingResumeCard.css
  • packages/dashboard/app/components/SetupWizardModal.css
  • packages/dashboard/app/components/SystemStatsModal.css
  • packages/dashboard/app/components/TaskIdIntegrityBanner.css
  • packages/dashboard/app/components/TodoView.css
  • packages/dashboard/app/styles.css
  • packages/dashboard/vitest.config.ts

Comment on lines +42 to +67
lines.forEach((line, index) => {
const lineNo = index + 1;

// calc() over a duration+easing pair is always invalid.
if (/calc\(\s*var\(--transition-/.test(line)) {
violations.push(`line ${lineNo}: calc() over a transition token — ${line.trim()}`);
return;
}

// animation shorthand: a transition token plus an explicit easing keyword
// substitutes to two easing functions and invalidates the declaration.
const animationMatch = line.match(/animation\s*:\s*([^;}]*)/);
if (animationMatch && /var\(--transition-/.test(animationMatch[1])) {
const valueWithoutToken = animationMatch[1].replace(/var\(--transition-[a-z]+\)/g, "");
if (EASING_KEYWORD.test(valueWithoutToken)) {
violations.push(
`line ${lineNo}: transition token + explicit easing in animation shorthand — ${line.trim()}`,
);
}
}

// animation-duration longhand cannot hold "0.3s ease" either.
if (/animation-duration\s*:\s*[^;}]*var\(--transition-/.test(line)) {
violations.push(`line ${lineNo}: transition token as animation-duration — ${line.trim()}`);
}
});

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 | ⚡ Quick win

Harden violation detection for multiline CSS declarations.

findViolations only inspects one line at a time, so multiline animation, animation-duration, or calc(var(--transition-...)) declarations can slip through undetected and reintroduce frozen animations without failing this test.

Suggested patch
 function findViolations(css: string): string[] {
   const violations: string[] = [];
-  const lines = css.split("\n");
-
-  lines.forEach((line, index) => {
-    const lineNo = index + 1;
-
-    // calc() over a duration+easing pair is always invalid.
-    if (/calc\(\s*var\(--transition-/.test(line)) {
-      violations.push(`line ${lineNo}: calc() over a transition token — ${line.trim()}`);
-      return;
-    }
-
-    // animation shorthand: a transition token plus an explicit easing keyword
-    // substitutes to two easing functions and invalidates the declaration.
-    const animationMatch = line.match(/animation\s*:\s*([^;}]*)/);
-    if (animationMatch && /var\(--transition-/.test(animationMatch[1])) {
-      const valueWithoutToken = animationMatch[1].replace(/var\(--transition-[a-z]+\)/g, "");
-      if (EASING_KEYWORD.test(valueWithoutToken)) {
-        violations.push(
-          `line ${lineNo}: transition token + explicit easing in animation shorthand — ${line.trim()}`,
-        );
-      }
-    }
-
-    // animation-duration longhand cannot hold "0.3s ease" either.
-    if (/animation-duration\s*:\s*[^;}]*var\(--transition-/.test(line)) {
-      violations.push(`line ${lineNo}: transition token as animation-duration — ${line.trim()}`);
-    }
-  });
+  const declarationRegex = /([a-z-]+)\s*:\s*([\s\S]*?);/gi;
+  let match: RegExpExecArray | null;
+  while ((match = declarationRegex.exec(css)) !== null) {
+    const [fullDecl, prop, value] = match;
+    const lineNo = css.slice(0, match.index).split("\n").length;
+    const decl = fullDecl.replace(/\s+/g, " ").trim();
+
+    if (/calc\(\s*var\(--transition-/.test(value)) {
+      violations.push(`line ${lineNo}: calc() over a transition token — ${decl}`);
+      continue;
+    }
+
+    if (prop === "animation" && /var\(--transition-/.test(value)) {
+      const valueWithoutToken = value.replace(/var\(--transition-[a-z]+\)/g, "");
+      if (EASING_KEYWORD.test(valueWithoutToken)) {
+        violations.push(
+          `line ${lineNo}: transition token + explicit easing in animation shorthand — ${decl}`,
+        );
+      }
+    }
+
+    if (prop === "animation-duration" && /var\(--transition-/.test(value)) {
+      violations.push(`line ${lineNo}: transition token as animation-duration — ${decl}`);
+    }
+  }
 
   return violations;
 }
🤖 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 `@packages/dashboard/app/__tests__/animation-duration-tokens.css.test.ts`
around lines 42 - 67, The current per-line checks in the lines.forEach loop miss
multiline declarations; update the test's findViolations logic to accumulate CSS
declarations across lines (e.g., join subsequent lines until a terminating ';'
or '}' or run the regexes against the full file/content chunk) before applying
the three detection rules (calc\(\s*var\(--transition-..., animation shorthand
check that uses EASING_KEYWORD, and animation-duration pattern). Modify the loop
that uses lines.forEach (or replace it) so animation/animation-duration/calc
patterns are evaluated on the assembled declaration string (referencing the
existing animationMatch handling and EASING_KEYWORD) to ensure multiline
declarations are detected.

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes all frozen dashboard animations caused by CSS IACVT (invalid-at-computed-value-time): --transition-* tokens encode a duration easing pair, and 15 animation declarations were reusing them as bare durations, silently resolving every affected animation property to none with no parse error or console warning.

  • Token split: new --duration-instant/fast/normal/slow bare-duration tokens are added to styles.css; the --transition-* tokens are now derived from them (var(--duration-slow) ease) so the two families cannot drift apart, and all existing transition: usages are unaffected.
  • 15 declaration fixes across 14 component CSS files: each var(--transition-*) (and calc(var(--transition-*) * N)) in an animation context is replaced with the corresponding --duration-* token, preserving effective durations.
  • Regression guard: animation-duration-tokens.css.test.ts sweeps every CSS file under app/ and fails on the three invalid shapes, registered in the foundation-ui vitest suite.

Confidence Score: 5/5

Safe to merge — all 15 broken animation declarations are corrected, the token redesign is backward-compatible, and a regression test enforces the new constraint going forward.

The CSS changes are mechanical and verified: each --transition-* reference in an animation context is replaced with its --duration-* counterpart, durations are numerically identical, and every existing transition: consumer is untouched. The regression test covers all three known violation shapes and is correctly registered in the test suite. The only gap found is a narrow false-positive scenario in the multi-animation comma-list check, which has no current manifestation in the codebase.

No files require special attention. The test logic at animation-duration-tokens.css.test.ts lines 53-60 has a theoretical false-positive for comma-separated multi-animation values, but no such pattern currently exists in the codebase.

Important Files Changed

Filename Overview
packages/dashboard/app/tests/animation-duration-tokens.css.test.ts New regression test scanning all CSS files for the three invalid patterns; logic is sound for single-animation declarations but has a narrow false-positive gap for comma-separated multi-animation values
packages/dashboard/app/styles.css Adds --duration-* bare-duration tokens and rewires --transition-* to be derived from them; backward-compatible with all existing transition: usages
packages/dashboard/app/components/NodesView.css Replaces two calc(var(--transition-slow) * N) custom-property values with calc(var(--duration-slow) * N), fixing IACVT invalidity on the local spin/pulse duration variables
packages/dashboard/vitest.config.ts Registers the new animation-duration-tokens test in the foundation-ui suite; correct placement, no environment mismatches
packages/dashboard/app/components/CustomProvidersSection.css Switches calc(var(--transition-slow) * 4) to calc(var(--duration-slow) * 4) in the spin animation, preserving effective 1.2s duration
docs/solutions/ui-bugs/css-animation-frozen-by-transition-token-shape-mismatch.md Detailed post-mortem documenting the IACVT mechanism, what prior fixes missed, and prevention guidance; accurate and well-structured

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["--transition-slow: 0.3s ease\n(before: single token)"] -->|"used as bare duration in animation:"| B["animation: spin 0.3s ease linear infinite\n(after var() substitution)"]
    B --> C["Two easing-functions: ease + linear\n→ IACVT: whole declaration dropped"]
    C --> D["animation: none — spinner frozen\n(no error, no DevTools warning)"]
    E["--duration-slow: 0.3s\n(new bare-duration token)"] -->|"used in animation:"| F["animation: spin 0.3s linear infinite\n(after var() substitution)"]
    F --> G["Valid — one easing, one duration\n→ animation runs correctly"]
    E -->|"derived into"| H["--transition-slow: var(--duration-slow) ease\n(transition token stays derived)"]
    H -->|"used in transition:"| I["transition: color 0.3s ease\n→ unchanged, still valid"]
    style D fill:#f66,color:#fff
    style G fill:#2a2,color:#fff
Loading

Reviews (2): Last reviewed commit: "docs: capture IACVT frozen-animation lea..." | Re-trigger Greptile

Comment on lines +42 to +67
lines.forEach((line, index) => {
const lineNo = index + 1;

// calc() over a duration+easing pair is always invalid.
if (/calc\(\s*var\(--transition-/.test(line)) {
violations.push(`line ${lineNo}: calc() over a transition token — ${line.trim()}`);
return;
}

// animation shorthand: a transition token plus an explicit easing keyword
// substitutes to two easing functions and invalidates the declaration.
const animationMatch = line.match(/animation\s*:\s*([^;}]*)/);
if (animationMatch && /var\(--transition-/.test(animationMatch[1])) {
const valueWithoutToken = animationMatch[1].replace(/var\(--transition-[a-z]+\)/g, "");
if (EASING_KEYWORD.test(valueWithoutToken)) {
violations.push(
`line ${lineNo}: transition token + explicit easing in animation shorthand — ${line.trim()}`,
);
}
}

// animation-duration longhand cannot hold "0.3s ease" either.
if (/animation-duration\s*:\s*[^;}]*var\(--transition-/.test(line)) {
violations.push(`line ${lineNo}: transition token as animation-duration — ${line.trim()}`);
}
});

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.

P2 findViolations doesn't strip inline CSS comments before pattern-matching

The findViolations function checks every raw line including the content of CSS block comments. A single-line comment like /* animation: spin var(--transition-slow) linear infinite; */ matches the animation\s*:\s* regex, and because valueWithoutToken would still contain linear, it would be counted as a violation — triggering a false-positive test failure. Any future developer who comments out an old animation declaration to document the before-state (e.g., as a migration guide or debug trace) would get a spurious test failure with no obvious cause. Stripping inline block comments (line.replace(/\/\*.*?\*\//g, "")) before the three pattern checks would prevent this.

Comment on lines +53 to +61
const animationMatch = line.match(/animation\s*:\s*([^;}]*)/);
if (animationMatch && /var\(--transition-/.test(animationMatch[1])) {
const valueWithoutToken = animationMatch[1].replace(/var\(--transition-[a-z]+\)/g, "");
if (EASING_KEYWORD.test(valueWithoutToken)) {
violations.push(
`line ${lineNo}: transition token + explicit easing in animation shorthand — ${line.trim()}`,
);
}
}

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.

P2 Multi-line animation declarations are silently skipped

The regex ([^;}]*) on a single line means that if an animation value is written across multiple lines (e.g., value starts on the next line after the colon), the capture group on the property line will be an empty string and the violation check is bypassed. While multi-line animation declarations are rare in this codebase, the guard test in "flags the frozen-spinner pattern" only exercises single-line inputs, so a future multi-line violation would go undetected without any indication the guard is incomplete. A brief comment in findViolations noting this limitation would at least make the gap intentional and visible.

Add docs/solutions/ui-bugs/ learning for the silent animation freeze caused
by transition tokens (duration+easing pairs) used as bare durations, and
update dashboard-guide.md design-token and pitfalls sections to cover the
new --duration-* tokens and the IACVT gotcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm
gsxdsm merged commit 433437e into main Jun 4, 2026
8 checks passed
@gsxdsm
gsxdsm deleted the gsxdsm/fix-spinners branch June 4, 2026 03:20
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.

1 participant