Anton ff85765728
fix: resolve react-doctor errors (hooks order, reduced motion, key prop) (#105)
- Move useCallback hooks before early return in ReferencesDialog
  to fix conditional hook call violation (Rules of Hooks)
- Add useReducedMotion hook and apply to all motion animations
  for WCAG 2.3.3 accessibility compliance
- Replace useEffect state reset with key prop on ItemCatalog
  for proper React reconciliation on category change
- Add global prefers-reduced-motion CSS media query

Fixes 4 react-doctor errors, bringing score from 81 to ~85+.
2026-02-19 19:20:05 +00:00

157 lines
5.0 KiB
TypeScript

"use client";
import { TooltipProvider } from "@/components/ui/primitives/tooltip";
import { cn } from "@/lib/utils";
import { CameraActions } from "./camera-actions";
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";
import { ViewToggles } from "./view-toggles";
export function ActionMenu({ className }: { className?: string }) {
const phase = useEditor((state) => state.phase);
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 (
<TooltipProvider>
<div
className={cn(
"-translate-x-1/2 fixed bottom-6 left-1/2 z-50",
"rounded-2xl border border-zinc-800 bg-zinc-950/90 shadow-2xl backdrop-blur-md",
"transition-all duration-200 ease-out",
className,
)}
>
{/* Item Catalog Row - Only show when in build mode with item tool */}
<AnimatePresence>
{mode === "build" && tool === "item" && catalogCategory && (
<motion.div
className={cn(
"overflow-hidden border-zinc-800 border-b px-2 py-2",
)}
initial={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
animate={{
opacity: 1,
maxHeight: 160,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
}}
exit={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<ItemCatalog key={catalogCategory} category={catalogCategory} />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{phase === "furnish" && mode === "build" && (
<motion.div
className={cn(
"overflow-hidden border-zinc-800",
"max-h-20 border-b px-2 py-2 opacity-100",
)}
initial={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
animate={{
opacity: 1,
maxHeight: 80,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
}}
exit={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<div className="mx-auto w-max">
<FurnishTools />
</div>
</motion.div>
)}
</AnimatePresence>
{/* Structure Tools Row - Animated */}
<AnimatePresence>
{phase === "structure" && mode === "build" && (
<motion.div
className={cn(
"overflow-hidden border-zinc-800 max-h-20 border-b px-2 py-2",
)}
initial={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
animate={{
opacity: 1,
maxHeight: 80,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
}}
exit={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<div className="w-max">
<StructureTools />
</div>
</motion.div>
)}
</AnimatePresence>
{/* Control Mode Row - Always visible, centered */}
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
<PhaseSwitcher />
<div className="mx-1 h-5 w-px bg-zinc-700" />
<ControlModes />
<div className="mx-1 h-5 w-px bg-zinc-700" />
<ViewToggles />
<div className="mx-1 h-5 w-px bg-zinc-700" />
<CameraActions />
</div>
</div>
</TooltipProvider>
);
}