From b6c15fe470fb8c46d15898c9597c4df9a611399a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 1 Jun 2026 23:29:21 -0700 Subject: [PATCH] FN-5855: fix frozen dashboard spinners Restore shared spinner animation behavior for dashboard AI activity indicators. - switch BackgroundTasksIndicator loaders to the shared animate-spin utility class - anchor shared spin utilities on SVG centers with transform-origin and transform-box rules - add CSS regression tests covering global spin keyframes and spinner utility styles Files changed: .../app/__tests__/spinner-animation.css.test.ts | 50 ++++++++++++++++++++++ .../app/components/BackgroundTasksIndicator.tsx | 9 ++-- packages/dashboard/app/styles.css | 7 +++ 3 files changed, 60 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5855 Fusion-Task-Lineage: b94d4333-2e78-4c4b-a304-5a13b797bbc6 --- .../__tests__/spinner-animation.css.test.ts | 50 +++++++++++++++++++ .../components/BackgroundTasksIndicator.tsx | 9 ++-- packages/dashboard/app/styles.css | 7 +++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 packages/dashboard/app/__tests__/spinner-animation.css.test.ts diff --git a/packages/dashboard/app/__tests__/spinner-animation.css.test.ts b/packages/dashboard/app/__tests__/spinner-animation.css.test.ts new file mode 100644 index 000000000..e4231946f --- /dev/null +++ b/packages/dashboard/app/__tests__/spinner-animation.css.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +function extractBlock(content: string, pattern: RegExp): string { + const match = content.match(pattern); + expect(match?.index).toBeDefined(); + + const start = match!.index! + match![0].length; + let index = start; + let depth = 1; + + while (index < content.length && depth > 0) { + if (content[index] === "{") depth += 1; + if (content[index] === "}") depth -= 1; + index += 1; + } + + expect(depth).toBe(0); + return content.slice(match!.index!, index); +} + +describe("global spinner animation utility", () => { + const css = readFileSync(resolve(__dirname, "../styles.css"), "utf8"); + + it("keeps top-level spin keyframes rotating to 360deg", () => { + const topLevelSpinBlock = extractBlock(css, /@keyframes\s+spin\s*\{/); + + expect(topLevelSpinBlock).toContain("transform: rotate(360deg);"); + expect(css.indexOf("@keyframes spin")).toBeLessThan(css.indexOf(":root {\n --bg:")); + }); + + it("keeps the shared animate-spin and spin utilities running infinitely", () => { + const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? ""; + const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? ""; + + expect(animateSpinBlock).toContain("animation: spin 1s linear infinite;"); + expect(spinBlock).toContain("animation: spin 1s linear infinite;"); + }); + + it("anchors SVG spinners around their own center", () => { + const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? ""; + const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? ""; + const svgSpinnerBlock = css.match(/svg\.animate-spin,\s*svg\.spin\s*\{[\s\S]*?\}/)?.[0] ?? ""; + + expect(animateSpinBlock).toContain("transform-origin: center;"); + expect(spinBlock).toContain("transform-origin: center;"); + expect(svgSpinnerBlock).toContain("transform-box: fill-box;"); + }); +}); diff --git a/packages/dashboard/app/components/BackgroundTasksIndicator.tsx b/packages/dashboard/app/components/BackgroundTasksIndicator.tsx index 205d16915..f8009805b 100644 --- a/packages/dashboard/app/components/BackgroundTasksIndicator.tsx +++ b/packages/dashboard/app/components/BackgroundTasksIndicator.tsx @@ -119,7 +119,7 @@ export function BackgroundTasksIndicator({ title={`${total} background AI task${total !== 1 ? "s" : ""}${needsInput > 0 ? ` (${needsInput} need${needsInput !== 1 ? "" : "s"} input)` : ""}`} > {generating > 0 && ( - + )} {needsInput > 0 && generating === 0 && } AI {total} @@ -188,11 +188,8 @@ export function BackgroundTasksIndicator({ {isGenerating && ( )} {isAwaiting && !activeElsewhere && ( diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index c6ffe4bb6..27a19d042 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -187,10 +187,17 @@ html { } .animate-spin { animation: spin 1s linear infinite; + transform-origin: center; } .spin { animation: spin 1s linear infinite; + transform-origin: center; +} + +svg.animate-spin, +svg.spin { + transform-box: fill-box; } :root {