Files
2026-07-13 13:08:41 +08:00

192 lines
5.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { Copy, MoreVertical, Trash2, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
canApplyComponentLayerAction,
type ComponentLayerAction,
} from "@/components/slide-editor/selection/layering";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export type ComponentActionsMenuActions = {
canUngroup: boolean;
componentIndex: number;
componentCount: number;
onDelete: () => void;
onDuplicate: () => void;
onLayerAction: (action: ComponentLayerAction) => void;
onUngroup: () => void;
};
const COMPONENT_LAYER_ACTIONS: Array<{
action: ComponentLayerAction;
label: string;
shortcut: string;
}> = [
{
action: "bring-to-front",
label: "Bring to Front",
shortcut: "⌥⌘]",
},
{
action: "bring-forward",
label: "Bring Forward",
shortcut: "⌘]",
},
{
action: "send-backward",
label: "Send Backward",
shortcut: "⌘[",
},
{
action: "send-to-back",
label: "Send Back",
shortcut: "⌥⌘[",
},
];
export function ComponentActionsMenu({
actions,
open,
onOpenChange,
}: {
actions: ComponentActionsMenuActions;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const menuOpen = open ?? uncontrolledOpen;
const setMenuOpen = onOpenChange ?? setUncontrolledOpen;
return (
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<button
type="button"
title="More"
aria-label="More"
className={cn(
"grid h-8 w-8 place-items-center rounded-[4px] border-0 bg-transparent font-manrope text-black hover:bg-[#F6F6F9]",
menuOpen && "bg-[#F6F6F9]",
)}
>
<MoreVertical
size={16}
className="text-black"
strokeWidth={1.33}
aria-hidden
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
data-template-v2-floating-toolbar="true"
data-inline-edit-ignore="true"
align="end"
sideOffset={12}
collisionPadding={8}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
className="z-[10001] box-border w-[206px] rounded-[12px] border border-[#EDEEEF] bg-white py-2 font-syne text-[14px] font-normal leading-normal tracking-[0.14px] text-[#191919] shadow-[0_6px_18px_rgba(16,24,40,0.08)]"
>
<ComponentActionsMenuItem
strong
icon={Copy}
label="Duplicate"
onClick={actions.onDuplicate}
/>
{COMPONENT_LAYER_ACTIONS.map(({ action, label, shortcut }) => {
const disabled = !canApplyComponentLayerAction(
actions.componentIndex,
actions.componentCount,
action,
);
return (
<ComponentActionsMenuItem
key={action}
disabled={disabled}
label={label}
shortcut={shortcut}
onClick={() => actions.onLayerAction(action)}
/>
);
})}
<DropdownMenuSeparator className="my-1 h-px bg-[#E7E8EC]" />
<ComponentActionsMenuItem
strong
icon={Trash2}
label="Delete Component"
onClick={actions.onDelete}
/>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function ComponentUngroupButton({
actions,
}: {
actions: ComponentActionsMenuActions;
}) {
if (!actions.canUngroup) return null;
return (
<button
type="button"
title="Ungroup"
onClick={actions.onUngroup}
className="inline-flex h-7 cursor-pointer items-center gap-1 rounded-[6px] px-2 font-manrope text-[14px] font-medium leading-4 text-[#191919] hover:bg-[#F6F6F9]"
>
<span>Ungroup</span>
</button>
);
}
function ComponentActionsMenuItem({
disabled,
icon: Icon = undefined,
label,
shortcut,
strong,
onClick,
}: {
disabled?: boolean;
icon?: LucideIcon;
label: string;
shortcut?: string;
strong?: boolean;
onClick: () => void;
}) {
return (
<DropdownMenuItem
disabled={disabled}
onSelect={onClick}
style={{ cursor: disabled ? "not-allowed" : "pointer" }}
className={cn(
"flex w-full cursor-default items-center gap-2 rounded-none px-4 py-2.5 text-left font-syne text-[14px] font-normal leading-normal tracking-[0.14px] text-[#191919] outline-none hover:bg-[#F6F6F9] focus:bg-[#F6F6F9] focus:text-[#191919]",
strong && "text-black",
disabled &&
"cursor-not-allowed text-[#A0A3AD] hover:bg-transparent focus:bg-transparent data-[disabled]:opacity-100",
)}
>
{Icon ? <Icon size={16} strokeWidth={1.33} aria-hidden /> : null}
<span>{label}</span>
{shortcut ? (
<span
className={cn(
"ml-auto inline-flex items-center justify-center rounded-[6px] bg-[#F6F6F9] px-1.5 py-1 font-manrope text-[14px] font-normal leading-none tracking-[0.14px] text-[#808080]",
disabled && "bg-[#F7F7FA] text-[#B0B3BB]",
)}
>
{shortcut}
</span>
) : null}
</DropdownMenuItem>
);
}