diff --git a/apps/editor/app/globals.css b/apps/editor/app/globals.css index 09ca6b85..9b1fb8f0 100644 --- a/apps/editor/app/globals.css +++ b/apps/editor/app/globals.css @@ -144,3 +144,14 @@ -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/apps/editor/components/ui/action-menu/index.tsx b/apps/editor/components/ui/action-menu/index.tsx index 0579c69b..7ad263bf 100644 --- a/apps/editor/components/ui/action-menu/index.tsx +++ b/apps/editor/components/ui/action-menu/index.tsx @@ -8,6 +8,7 @@ import { ControlModes } from "./control-modes"; import { PhaseSwitcher } from "./phase-switcher"; import { StructureTools } from "./structure-tools"; import useEditor from "@/store/use-editor"; +import { useReducedMotion } from "@/hooks/use-reduced-motion"; import { AnimatePresence, motion } from "motion/react"; import { ItemCatalog } from "../item-catalog/item-catalog"; import { FurnishTools } from "./furnish-tools"; @@ -18,6 +19,8 @@ export function ActionMenu({ className }: { className?: string }) { const mode = useEditor((state) => state.mode); const tool = useEditor((state) => state.tool); const catalogCategory = useEditor((state) => state.catalogCategory); + const reducedMotion = useReducedMotion(); + const transition = reducedMotion ? { duration: 0 } : undefined; return ( @@ -57,8 +60,9 @@ export function ActionMenu({ className }: { className?: string }) { paddingBottom: 0, borderBottomWidth: 0, }} + transition={transition} > - + )} @@ -91,6 +95,7 @@ export function ActionMenu({ className }: { className?: string }) { paddingBottom: 0, borderBottomWidth: 0, }} + transition={transition} >
@@ -127,6 +132,7 @@ export function ActionMenu({ className }: { className?: string }) { paddingBottom: 0, borderBottomWidth: 0, }} + transition={transition} >
diff --git a/apps/editor/components/ui/item-catalog/item-catalog.tsx b/apps/editor/components/ui/item-catalog/item-catalog.tsx index 3c439e64..ebf3a22f 100644 --- a/apps/editor/components/ui/item-catalog/item-catalog.tsx +++ b/apps/editor/components/ui/item-catalog/item-catalog.tsx @@ -21,12 +21,6 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) { const [activePlacementTag, setActivePlacementTag] = useState(null); const [activeFunctionalTag, setActiveFunctionalTag] = useState(null); - // Reset tag filters when category changes - useEffect(() => { - setActivePlacementTag(null); - setActiveFunctionalTag(null); - }, [category]); - const categoryItems = CATALOG_ITEMS.filter( (item) => item.category === category, ); diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx index 033bce12..6d384fff 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/references-dialog.tsx @@ -38,15 +38,6 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial const scanInputRef = useRef(null) const guideInputRef = useRef(null) - const level = nodes[levelId as AnyNodeId] as LevelNode | undefined - if (!level) return null - - // Find all scan and guide children of this level - const references = Object.values(nodes).filter( - (node): node is ScanNode | GuideNode => - (node.type === 'scan' || node.type === 'guide') && node.parentId === levelId, - ) - const handleAddScan = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0] @@ -100,6 +91,15 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial [deleteNode], ) + const level = nodes[levelId as AnyNodeId] as LevelNode | undefined + if (!level) return null + + // Find all scan and guide children of this level + const references = Object.values(nodes).filter( + (node): node is ScanNode | GuideNode => + (node.type === 'scan' || node.type === 'guide') && node.parentId === levelId, + ) + return ( diff --git a/apps/editor/hooks/use-reduced-motion.ts b/apps/editor/hooks/use-reduced-motion.ts new file mode 100644 index 00000000..b46e63dd --- /dev/null +++ b/apps/editor/hooks/use-reduced-motion.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react' + +/** + * Returns true when the user has requested reduced motion via OS settings. + * Useful for disabling animations (WCAG 2.3.3). + */ +export function useReducedMotion(): boolean { + const [reducedMotion, setReducedMotion] = useState(false) + + useEffect(() => { + const mql = window.matchMedia('(prefers-reduced-motion: reduce)') + setReducedMotion(mql.matches) + + const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches) + mql.addEventListener('change', handler) + return () => mql.removeEventListener('change', handler) + }, []) + + return reducedMotion +}