diff --git a/src/app/dashboard/_components/invoice-status-chart.tsx b/src/app/dashboard/_components/invoice-status-chart.tsx
index e720384..fbf5db3 100644
--- a/src/app/dashboard/_components/invoice-status-chart.tsx
+++ b/src/app/dashboard/_components/invoice-status-chart.tsx
@@ -1,6 +1,7 @@
"use client";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
+import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
@@ -51,6 +52,12 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
paid: "hsl(142, 76%, 36%)", // green
overdue: "hsl(0, 84%, 60%)", // red
};
+ // Animation / motion preferences
+ const { prefersReducedMotion, animationSpeedMultiplier } =
+ useAnimationPreferences();
+ const pieAnimationDuration = Math.round(
+ 600 / (animationSpeedMultiplier || 1),
+ );
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
@@ -113,6 +120,9 @@ export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) {
outerRadius={80}
stroke="none"
dataKey="count"
+ isAnimationActive={!prefersReducedMotion}
+ animationDuration={pieAnimationDuration}
+ animationEasing="ease-out"
>
{chartData.map((entry, index) => (
|
diff --git a/src/app/dashboard/_components/revenue-chart.tsx b/src/app/dashboard/_components/revenue-chart.tsx
index 79b24f9..c256d61 100644
--- a/src/app/dashboard/_components/revenue-chart.tsx
+++ b/src/app/dashboard/_components/revenue-chart.tsx
@@ -10,6 +10,7 @@ import {
} from "recharts";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import type { StoredInvoiceStatus } from "~/types/invoice";
+import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
interface Invoice {
id: string;
@@ -99,6 +100,8 @@ export function RevenueChart({ invoices }: RevenueChartProps) {
return null;
};
+ const { prefersReducedMotion, animationSpeedMultiplier } =
+ useAnimationPreferences();
if (chartData.length === 0) {
return (
@@ -147,6 +150,11 @@ export function RevenueChart({ invoices }: RevenueChartProps) {
stroke="hsl(0, 0%, 60%)"
strokeWidth={2}
fill="url(#revenueGradient)"
+ isAnimationActive={!prefersReducedMotion}
+ animationDuration={Math.round(
+ 600 / (animationSpeedMultiplier ?? 1),
+ )}
+ animationEasing="ease-out"
/>
diff --git a/src/app/dashboard/settings/_components/settings-content.tsx b/src/app/dashboard/settings/_components/settings-content.tsx
index a8eaf8a..bdaaa14 100644
--- a/src/app/dashboard/settings/_components/settings-content.tsx
+++ b/src/app/dashboard/settings/_components/settings-content.tsx
@@ -59,6 +59,9 @@ import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { api } from "~/trpc/react";
+import { Switch } from "~/components/ui/switch";
+import { Slider } from "~/components/ui/slider";
+import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
export function SettingsContent() {
const { data: session } = useSession();
@@ -76,6 +79,25 @@ export function SettingsContent() {
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ // Animation preferences via provider (centralized)
+ const {
+ prefersReducedMotion,
+ animationSpeedMultiplier,
+ updatePreferences,
+ isUpdating: animationPrefsUpdating,
+ setPrefersReducedMotion,
+ setAnimationSpeedMultiplier,
+ } = useAnimationPreferences();
+
+ const handleSaveAnimationPreferences = (e: React.FormEvent) => {
+ e.preventDefault();
+ updatePreferences({
+ prefersReducedMotion,
+ animationSpeedMultiplier,
+ });
+ toast.success("Animation preferences updated");
+ };
+
// Queries
const { data: profile, refetch: refetchProfile } =
api.settings.getProfile.useQuery();
@@ -271,6 +293,8 @@ export function SettingsContent() {
}
}, [profile?.name, name]);
+ // (Removed direct DOM mutation; provider handles applying preferences globally)
+
const dataStatItems = [
{
label: "Clients",
@@ -498,6 +522,99 @@ export function SettingsContent() {
+ {/* Accessibility & Animation */}
+
+
+
+
+ Accessibility & Animation
+
+
+
+
+
+
+
{/* Data Management */}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b08bd4e..5ef964c 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -6,6 +6,7 @@ import { Geist_Mono } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/sonner";
+import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple",
@@ -24,10 +25,22 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
-
+
+
+ {/* Inline early animation preference script to avoid FOUC */}
+