mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 00:06:36 -05:00
Add user-controlled animation preferences and reduce motion support
- Persist prefersReducedMotion and animationSpeedMultiplier in user profile - Provide UI controls to toggle reduce motion and adjust animation speed globally - Centralize animation preferences via provider and useAnimationPreferences hook - Apply preferences to charts’ animations (duration, enabled/disabled) - Inline script in layout to apply preferences early and avoid FOUC - Update CSS to respect user preference with reduced motion overrides and variable animation speeds
This commit is contained in:
@@ -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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Accessibility & Animation */}
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center gap-2">
|
||||
<Info className="text-primary h-5 w-5" />
|
||||
Accessibility & Animation
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSaveAnimationPreferences} className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Reduce Motion</Label>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
Turn this on to reduce or remove non-essential animations and
|
||||
transitions.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={prefersReducedMotion}
|
||||
onCheckedChange={(checked) =>
|
||||
setPrefersReducedMotion(Boolean(checked))
|
||||
}
|
||||
aria-label="Toggle reduced motion"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="mb-0">Animation Speed Multiplier</Label>
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
{prefersReducedMotion
|
||||
? "1.00x (locked)"
|
||||
: `${animationSpeedMultiplier.toFixed(2)}x`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
Adjust global animation duration scaling. Lower values (0.25×,
|
||||
0.5×, 0.75×) slow animations; higher values (2×, 3×, 4×) speed
|
||||
them up.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Slider (desktop / larger screens) */}
|
||||
<div className="hidden sm:block">
|
||||
<Slider
|
||||
value={[animationSpeedMultiplier]}
|
||||
min={0.25}
|
||||
max={4}
|
||||
step={0.25}
|
||||
ticks={[0.25, 0.5, 0.75, 1, 2, 3, 4]}
|
||||
formatTick={(t) => (t === 1 ? "1x" : `${t}x`)}
|
||||
onValueChange={(v: number[]) =>
|
||||
setAnimationSpeedMultiplier(v[0] ?? 1)
|
||||
}
|
||||
aria-label="Animation speed multiplier"
|
||||
className="mt-1"
|
||||
disabled={prefersReducedMotion}
|
||||
/>
|
||||
</div>
|
||||
{/* Dropdown fallback (small screens) */}
|
||||
<div className="block sm:hidden">
|
||||
<select
|
||||
className="bg-background border-border text-foreground w-full rounded-md border px-2 py-2 text-sm disabled:opacity-60"
|
||||
value={animationSpeedMultiplier}
|
||||
disabled={prefersReducedMotion}
|
||||
onChange={(e) =>
|
||||
setAnimationSpeedMultiplier(
|
||||
parseFloat(e.target.value) || 1,
|
||||
)
|
||||
}
|
||||
aria-label="Animation speed multiplier select"
|
||||
>
|
||||
{[0.25, 0.5, 0.75, 1, 2, 3, 4].map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v === 1 ? "1x (default)" : `${v}x`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={animationPrefsUpdating}
|
||||
variant="default"
|
||||
>
|
||||
{animationPrefsUpdating ? "Saving..." : "Save Preferences"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Data Management */}
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user