diff --git a/apps/loopover-miner-ui/src/components/chat/typing-indicator.test.tsx b/apps/loopover-miner-ui/src/components/chat/typing-indicator.test.tsx
new file mode 100644
index 0000000000..97b6024b35
--- /dev/null
+++ b/apps/loopover-miner-ui/src/components/chat/typing-indicator.test.tsx
@@ -0,0 +1,23 @@
+import { render } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { TypingIndicator } from "./typing-indicator";
+
+// Regression for #8303: the three animate-bounce dots must each pair their animation with
+// motion-reduce:animate-none so a user with the OS "reduce motion" preference set sees a static indicator
+// instead of a bouncing one -- matching the guard skeleton.tsx / Spinner already provide.
+describe("TypingIndicator respects prefers-reduced-motion (#8303)", () => {
+ it("renders three bouncing dots that each carry motion-reduce:animate-none", () => {
+ const { container } = render();
+ const dots = container.querySelectorAll(".animate-bounce");
+ expect(dots.length).toBe(3);
+ for (const dot of dots) {
+ expect(dot.className).toContain("motion-reduce:animate-none");
+ }
+ });
+
+ it("renders nothing when not composing (unchanged behavior)", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx b/apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx
index 53d33e65ac..5cecec1b09 100644
--- a/apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx
+++ b/apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx
@@ -16,13 +16,16 @@ export function TypingIndicator({
{label}
+
-
);
}
diff --git a/apps/loopover-miner-ui/src/components/streaming-text.tsx b/apps/loopover-miner-ui/src/components/streaming-text.tsx
index f9398c60ae..4af577184e 100644
--- a/apps/loopover-miner-ui/src/components/streaming-text.tsx
+++ b/apps/loopover-miner-ui/src/components/streaming-text.tsx
@@ -32,7 +32,7 @@ export function StreamingText({ source, className }: { source: ChunkSource | nul
{text}
{status === "streaming" && !reducedMotion ? (
-
+
▍
) : null}
diff --git a/apps/loopover-miner-ui/src/streaming-text.test.tsx b/apps/loopover-miner-ui/src/streaming-text.test.tsx
index ad84beb7d4..654d5960ec 100644
--- a/apps/loopover-miner-ui/src/streaming-text.test.tsx
+++ b/apps/loopover-miner-ui/src/streaming-text.test.tsx
@@ -61,6 +61,9 @@ describe("StreamingText (#6516)", () => {
await src.push("typing…");
await waitFor(() => expect(screen.getByText(/typing…/)).toBeTruthy());
expect(caret()).not.toBeNull(); // still streaming → caret present under full motion
+ // #8303: the caret's animate-pulse is also paired with motion-reduce:animate-none as a CSS-level
+ // fallback, complementing the JS usePrefersReducedMotion guard that already omits it entirely.
+ expect(caret()?.className).toContain("motion-reduce:animate-none");
});
it("suppresses the caret under prefers-reduced-motion but still reaches the full text and done", async () => {
diff --git a/packages/loopover-ui-kit/src/components/accordion.test.tsx b/packages/loopover-ui-kit/src/components/accordion.test.tsx
new file mode 100644
index 0000000000..f9feee9786
--- /dev/null
+++ b/packages/loopover-ui-kit/src/components/accordion.test.tsx
@@ -0,0 +1,33 @@
+import { render } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "./accordion";
+
+// Regression for #8303: the Radix content/transition components in @loopover/ui-kit must pair their
+// animate-* utilities with motion-reduce:animate-none, matching skeleton.tsx / state-views.tsx, so a user
+// with the OS "reduce motion" preference set does not get the fade/zoom/slide/accordion animation.
+// AccordionContent is a representative Radix content component (its animate-accordion-up/down utilities are
+// the ones the issue calls out) and renders inline when its item is open.
+describe("AccordionContent respects prefers-reduced-motion (#8303)", () => {
+ it("carries motion-reduce:animate-none alongside its animate-accordion utilities", () => {
+ const { getByText } = render(
+
+
+ Trigger
+ Body
+
+ ,
+ );
+ // The Radix Content wraps the inner padding div that holds the text.
+ const content = getByText("Body").parentElement as HTMLElement;
+ expect(content.className).toContain(
+ "data-[state=open]:animate-accordion-down",
+ );
+ expect(content.className).toContain("motion-reduce:animate-none");
+ });
+});
diff --git a/packages/loopover-ui-kit/src/components/accordion.tsx b/packages/loopover-ui-kit/src/components/accordion.tsx
index aa196ab781..b17199a2a7 100644
--- a/packages/loopover-ui-kit/src/components/accordion.tsx
+++ b/packages/loopover-ui-kit/src/components/accordion.tsx
@@ -44,7 +44,7 @@ const AccordionContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
{children}
diff --git a/packages/loopover-ui-kit/src/components/alert-dialog.tsx b/packages/loopover-ui-kit/src/components/alert-dialog.tsx
index c5d6547ae7..5427c29ef6 100644
--- a/packages/loopover-ui-kit/src/components/alert-dialog.tsx
+++ b/packages/loopover-ui-kit/src/components/alert-dialog.tsx
@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
-
+
)}
diff --git a/packages/loopover-ui-kit/src/components/menubar.tsx b/packages/loopover-ui-kit/src/components/menubar.tsx
index 4ac7a1320c..e977fd2d72 100644
--- a/packages/loopover-ui-kit/src/components/menubar.tsx
+++ b/packages/loopover-ui-kit/src/components/menubar.tsx
@@ -92,7 +92,7 @@ const MenubarSubContent = React.forwardRef<
(({ className, ...props }, ref) => (