Aymeric Rabot d0e0fe7e1e UX polish round 4: action button component, keyboard shortcuts dialog, and build fixes
- Add reusable ActionButton component with tooltip and shortcut display
- Add keyboard shortcuts dialog in settings panel
- Fix ButtonProps import error (derive type from Button component)
- Fix AnyNodeId type mismatch in tree-node-actions visibility toggle
- Various UI refinements across action menu, sidebar, and editor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:19:13 -05:00

137 lines
3.6 KiB
TypeScript

"use client";
import Image from "next/image";
import { ActionButton } from "./action-button";
import { Pencil, Trash2, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import useEditor, { Mode, Phase } from "@/store/use-editor";
type ModeConfig = {
id: Mode;
icon?: LucideIcon;
imageSrc?: string;
label: string;
shortcut: string;
color: string;
activeColor: string;
};
// All available control modes
const allModes: ModeConfig[] = [
{
id: "select",
imageSrc: "/icons/select.png",
label: "Select",
shortcut: "V",
color: "hover:bg-blue-500/20 hover:text-blue-400",
activeColor: "bg-blue-500/20 text-blue-400",
},
{
id: "edit",
icon: Pencil,
label: "Edit",
shortcut: "E",
color: "hover:bg-orange-500/20 hover:text-orange-400",
activeColor: "bg-orange-500/20 text-orange-400",
},
{
id: "build",
imageSrc: "/icons/build.png",
label: "Build",
shortcut: "B",
color: "hover:bg-green-500/20 hover:text-green-400",
activeColor: "bg-green-500/20 text-green-400",
},
{
id: "delete",
icon: Trash2,
label: "Delete",
shortcut: "D",
color: "hover:bg-red-500/20 hover:text-red-400",
activeColor: "bg-red-500/20 text-red-400",
},
// {
// id: 'painting',
// icon: Paintbrush,
// label: 'Painting',
// shortcut: 'P',
// color: 'hover:bg-cyan-500/20 hover:text-cyan-400',
// activeColor: 'bg-cyan-500/20 text-cyan-400',
// },
// {
// id: 'guide',
// icon: Image,
// label: 'Guide',
// shortcut: 'G',
// color: 'hover:bg-purple-500/20 hover:text-purple-400',
// activeColor: 'bg-purple-500/20 text-purple-400',
// },
];
// Define which modes are available in each editor mode
const modesByPhase: Record<Phase, Mode[]> = {
site: ["select", "edit"],
structure: ["select", "delete", "build"],
furnish: ["select", "delete", "build"],
};
export function ControlModes() {
const mode = useEditor((state) => state.mode);
const phase = useEditor((state) => state.phase);
const setMode = useEditor((state) => state.setMode);
const availableModeIds = modesByPhase[phase];
const availableModes = allModes.filter((m) =>
availableModeIds.includes(m.id)
);
const handleModeClick = (mode: Mode) => {
setMode(mode);
};
return (
<div className="flex items-center gap-1">
{availableModes.map((m) => {
const Icon = m.icon;
const isActive = mode === m.id;
const isImageMode = Boolean(m.imageSrc);
return (
<ActionButton
key={m.id}
label={m.label}
shortcut={m.shortcut}
className={cn(
"text-muted-foreground",
!isImageMode && !isActive && m.color,
!isImageMode && isActive && m.activeColor,
isImageMode && isActive && "bg-white/10 hover:bg-white/10",
isImageMode && !isActive && "hover:bg-white/5"
)}
onClick={() => handleModeClick(m.id)}
size="icon"
variant="ghost"
>
{m.imageSrc ? (
<Image
alt={m.label}
className={cn(
"h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200",
!isActive && "opacity-60 grayscale",
isActive && "opacity-100 grayscale-0"
)}
height={28}
src={m.imageSrc}
width={28}
/>
) : (
Icon && <Icon className="h-5 w-5" />
)}
</ActionButton>
);
})}
</div>
);
}