resend-editor
Version:
A drag-and-drop email editor for React applications
1,232 lines (1,231 loc) • 110 kB
JavaScript
import * as React8 from 'react';
import { useSensors, useSensor, PointerSensor, DndContext, pointerWithin, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
import { X, BoldIcon, ItalicIcon, UnderlineIcon, TypeIcon, ChevronDownIcon, PaletteIcon, RotateCcwIcon, RotateCwIcon, MonitorIcon, SmartphoneIcon, ChevronRightIcon, ChevronLeftIcon, LayoutTemplateIcon, LayoutListIcon, PanelTopIcon, PanelBottomIcon, TrashIcon, PlusIcon, Heading1Icon, RectangleHorizontalIcon, LinkIcon, ImageIcon, MinusIcon, SpaceIcon, BoxIcon, LayoutIcon, Grid3x3Icon, ColumnsIcon, Footprints, ListIcon, Share2Icon, CodeIcon, FileCodeIcon, FileTextIcon, GripVerticalIcon, UploadIcon, Loader2Icon, UnfoldHorizontalIcon } from 'lucide-react';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import { CSS } from '@dnd-kit/utilities';
import DOMPurify from 'dompurify';
import * as LabelPrimitive from '@radix-ui/react-label';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import Cropper from 'react-easy-crop';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { render, Html, Head, Body, Container, Section, Hr, Img, Button as Button$1, Heading, Text } from '@react-email/components';
import { Editor } from '@monaco-editor/react';
// src/EmailEditor.tsx
function cn(...inputs) {
return twMerge(clsx(inputs));
}
var buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 cursor-pointer",
destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}) {
const Comp = asChild ? Slot : "button";
return /* @__PURE__ */ jsx(
Comp,
{
"data-slot": "button",
className: cn(buttonVariants({ variant, size, className })),
...props
}
);
}
function PaletteItem({ type, label, icon }) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `palette-${type}`,
data: { type, isNew: true }
});
return /* @__PURE__ */ jsxs(
"div",
{
ref: setNodeRef,
...listeners,
...attributes,
className: `
flex flex-col items-center gap-2 p-4 rounded-lg border border-border
bg-card hover:bg-accent cursor-grab active:cursor-grabbing
transition-colors
${isDragging ? "opacity-50" : ""}
`,
children: [
/* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: icon }),
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: label })
]
}
);
}
function ComponentPalette() {
const [isCollapsed, setIsCollapsed] = React8.useState(false);
const basicComponents = [
{ type: "text", label: "Text", icon: /* @__PURE__ */ jsx(TypeIcon, { className: "size-4" }) },
{ type: "heading", label: "Heading", icon: /* @__PURE__ */ jsx(Heading1Icon, { className: "size-4" }) },
{ type: "button", label: "Button", icon: /* @__PURE__ */ jsx(RectangleHorizontalIcon, { className: "size-4" }) },
{ type: "link", label: "Link", icon: /* @__PURE__ */ jsx(LinkIcon, { className: "size-4" }) },
{ type: "image", label: "Image", icon: /* @__PURE__ */ jsx(ImageIcon, { className: "size-4" }) },
{ type: "divider", label: "Divider", icon: /* @__PURE__ */ jsx(MinusIcon, { className: "size-4" }) },
{ type: "spacer", label: "Spacer", icon: /* @__PURE__ */ jsx(SpaceIcon, { className: "size-4" }) }
];
const layoutComponents = [
{ type: "container", label: "Container", icon: /* @__PURE__ */ jsx(BoxIcon, { className: "size-4" }) },
{ type: "section", label: "Section", icon: /* @__PURE__ */ jsx(LayoutIcon, { className: "size-4" }) },
{ type: "grid", label: "Grid", icon: /* @__PURE__ */ jsx(Grid3x3Icon, { className: "size-4" }) },
{ type: "header", label: "Header", icon: /* @__PURE__ */ jsx(ColumnsIcon, { className: "size-4" }) },
{ type: "footer", label: "Footer", icon: /* @__PURE__ */ jsx(Footprints, { className: "size-4" }) }
];
const advancedComponents = [
{ type: "list", label: "List", icon: /* @__PURE__ */ jsx(ListIcon, { className: "size-4" }) },
{ type: "social", label: "Social", icon: /* @__PURE__ */ jsx(Share2Icon, { className: "size-4" }) },
{ type: "code-inline", label: "Code", icon: /* @__PURE__ */ jsx(CodeIcon, { className: "size-4" }) },
{ type: "code-block", label: "Code Block", icon: /* @__PURE__ */ jsx(FileCodeIcon, { className: "size-4" }) },
{ type: "markdown", label: "Markdown", icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "size-4" }) }
];
if (isCollapsed) {
return /* @__PURE__ */ jsx("div", { className: "border-r bg-muted/30 flex items-center justify-center", children: /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
onClick: () => setIsCollapsed(false),
className: "h-12",
children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-4" })
}
) });
}
return /* @__PURE__ */ jsxs("div", { className: "w-64 border-r bg-muted/30 p-4 overflow-y-auto relative group", children: [
/* @__PURE__ */ jsx(
"div",
{
className: "absolute top-0 right-0 w-1 h-full cursor-pointer hover:bg-blue-500/50 transition-colors",
onClick: () => setIsCollapsed(true),
title: "Click to collapse"
}
),
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
onClick: () => setIsCollapsed(true),
className: "absolute top-2 right-2 h-8 w-8",
children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4" })
}
),
/* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm mb-3", children: "Basic" }),
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: basicComponents.map((component) => /* @__PURE__ */ jsx(PaletteItem, { ...component }, component.type)) })
] }),
/* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm mb-3", children: "Layout" }),
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: layoutComponents.map((component) => /* @__PURE__ */ jsx(PaletteItem, { ...component }, component.type)) })
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm mb-3", children: "Advanced" }),
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: advancedComponents.map((component) => /* @__PURE__ */ jsx(PaletteItem, { ...component }, component.type)) })
] }),
/* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm mb-3", children: "Blocks" }),
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-2", children: [
/* @__PURE__ */ jsx(PaletteItem, { type: "hero", label: "Hero Section", icon: /* @__PURE__ */ jsx(LayoutTemplateIcon, { className: "size-4" }) }),
/* @__PURE__ */ jsx(PaletteItem, { type: "features", label: "Features Grid", icon: /* @__PURE__ */ jsx(LayoutListIcon, { className: "size-4" }) }),
/* @__PURE__ */ jsx(PaletteItem, { type: "header", label: "Header", icon: /* @__PURE__ */ jsx(PanelTopIcon, { className: "size-4" }) }),
/* @__PURE__ */ jsx(PaletteItem, { type: "footer", label: "Footer", icon: /* @__PURE__ */ jsx(PanelBottomIcon, { className: "size-4" }) })
] })
] })
] });
}
function InlineToolbar({ onFormat }) {
const handleAction = (e, action) => {
e.preventDefault();
e.stopPropagation();
action();
};
return /* @__PURE__ */ jsxs("div", { "data-inline-toolbar": "true", className: "absolute -top-12 left-0 z-50 flex items-center gap-1 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-md shadow-lg p-1 animate-in fade-in slide-in-from-bottom-2", children: [
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
className: "h-8 w-8 hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-200",
onMouseDown: (e) => handleAction(e, () => onFormat("bold")),
title: "Bold",
children: /* @__PURE__ */ jsx(BoldIcon, { className: "size-4" })
}
),
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
className: "h-8 w-8 hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-200",
onMouseDown: (e) => handleAction(e, () => onFormat("italic")),
title: "Italic",
children: /* @__PURE__ */ jsx(ItalicIcon, { className: "size-4" })
}
),
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
className: "h-8 w-8 hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-200",
onMouseDown: (e) => handleAction(e, () => onFormat("underline")),
title: "Underline",
children: /* @__PURE__ */ jsx(UnderlineIcon, { className: "size-4" })
}
),
/* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-border dark:bg-neutral-700 mx-1" }),
/* @__PURE__ */ jsxs("div", { className: "relative flex items-center group", children: [
/* @__PURE__ */ jsx(TypeIcon, { className: "absolute left-2 size-3 text-muted-foreground pointer-events-none" }),
/* @__PURE__ */ jsx(
"select",
{
className: "h-8 w-14 pl-7 pr-1 text-xs bg-transparent hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-md appearance-none outline-none cursor-pointer text-neutral-700 dark:text-neutral-200 focus:ring-0 border-none",
onChange: (e) => onFormat("fontSize", e.target.value),
defaultValue: "3",
title: "Font Size",
children: [1, 2, 3, 4, 5, 6, 7].map((size) => /* @__PURE__ */ jsx("option", { value: size, children: size }, size))
}
),
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "absolute right-1 size-3 text-muted-foreground pointer-events-none opacity-50 group-hover:opacity-100" })
] }),
/* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center w-8 h-8 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-md cursor-pointer", children: [
/* @__PURE__ */ jsx(PaletteIcon, { className: "absolute size-4 text-neutral-700 dark:text-neutral-200 pointer-events-none" }),
/* @__PURE__ */ jsx(
"input",
{
type: "color",
className: "opacity-0 w-full h-full cursor-pointer",
onChange: (e) => onFormat("foreColor", e.target.value),
title: "Text Color"
}
)
] })
] });
}
function SortableEmailComponent({ component, isSelected, selectedId, onSelect, onUpdateContent }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({ id: component.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || "transform 250ms ease",
opacity: isDragging ? 0.5 : 1
};
return /* @__PURE__ */ jsxs(
"div",
{
ref: setNodeRef,
style,
className: `
relative group pl-10
${isSelected ? "ring-2 ring-blue-500 ring-offset-2" : ""}
`,
onClick: (e) => {
e.stopPropagation();
onSelect(component.id);
},
children: [
/* @__PURE__ */ jsx(
"div",
{
...attributes,
...listeners,
className: "absolute left-2 top-1/2 -translate-y-1/2 cursor-grab active:cursor-grabbing z-10 p-1 rounded hover:bg-muted/50 transition-all opacity-0 group-hover:opacity-100",
title: "Drag to reorder",
children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-4 text-muted-foreground hover:text-foreground" })
}
),
/* @__PURE__ */ jsx(
ComponentRenderer,
{
component,
onUpdateContent,
onSelect,
isSelected,
selectedId
}
)
]
}
);
}
function GridColumn({ column, index, isSelected, selectedId, onSelect, onUpdateContent }) {
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
id: `${column.id}-dropzone`,
data: { parentId: column.id }
});
return /* @__PURE__ */ jsx(
"div",
{
className: `border border-dashed rounded cursor-pointer transition-colors ${isSelected ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : isOver ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-500 ring-offset-2" : "border-neutral-300 dark:border-neutral-600 bg-neutral-50 dark:bg-neutral-700/50 hover:border-neutral-400 dark:hover:border-neutral-500"}`,
style: {
flex: 1,
minHeight: "100px",
padding: "12px"
},
onClick: (e) => {
e.stopPropagation();
onSelect(column.id);
},
children: /* @__PURE__ */ jsxs("div", { ref: setDroppableRef, children: [
isOver && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-blue-500/10 z-20 pointer-events-none", children: /* @__PURE__ */ jsx("div", { className: "bg-blue-500 text-white px-3 py-1 rounded-full text-sm font-medium shadow-sm", children: "Drop here" }) }),
!column.children || column.children.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center h-20 text-xs text-muted-foreground dark:text-neutral-400", children: [
/* @__PURE__ */ jsx(PlusIcon, { className: "size-3 mr-1" }),
"Column ",
index + 1
] }) : /* @__PURE__ */ jsx(
SortableContext,
{
items: column.children.map((c) => c.id),
strategy: verticalListSortingStrategy,
children: column.children.map((child) => /* @__PURE__ */ jsx(
SortableEmailComponent,
{
component: child,
isSelected: selectedId === child.id,
selectedId,
onSelect,
onUpdateContent
},
child.id
))
}
)
] })
}
);
}
var TextRenderer = React8.memo(({ component, isEditing, contentRef, handleClick, handleBlur, handleFormat }) => /* @__PURE__ */ jsxs("div", { className: "relative", onBlur: handleBlur, children: [
isEditing && /* @__PURE__ */ jsx(InlineToolbar, { onFormat: handleFormat }),
/* @__PURE__ */ jsx(
"p",
{
ref: contentRef,
onClick: handleClick,
contentEditable: isEditing,
suppressContentEditableWarning: true,
"aria-label": "Editable text",
style: {
margin: 0,
outline: "none",
cursor: isEditing ? "text" : "pointer",
whiteSpace: "pre-wrap",
color: component.props.color || "#000000",
...component.props.style
},
dir: "ltr",
dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(component.props.content || "Add your text here...") }
}
)
] }));
var HeadingRenderer = React8.memo(({ component, isEditing, contentRef, handleClick, handleBlur, handleFormat }) => {
const level = component.props.level || 2;
const fontSizes = {
1: "32px",
2: "24px",
3: "20px",
4: "18px",
5: "16px",
6: "14px"
};
const fontSize = fontSizes[level];
const HeadingTag = `h${level}`;
return /* @__PURE__ */ jsxs("div", { className: "relative", onBlur: handleBlur, children: [
isEditing && /* @__PURE__ */ jsx(InlineToolbar, { onFormat: handleFormat }),
React8.createElement(
HeadingTag,
{
ref: contentRef,
onClick: handleClick,
contentEditable: isEditing,
suppressContentEditableWarning: true,
dir: "ltr",
"aria-label": "Editable heading",
style: {
margin: 0,
fontSize,
fontWeight: "bold",
outline: "none",
cursor: isEditing ? "text" : "pointer",
whiteSpace: "pre-wrap",
color: component.props.color || "#000000",
...component.props.style
},
dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(component.props.content || "Your Heading") }
}
)
] });
});
var ButtonRenderer = React8.memo(({ component, isEditing, contentRef, handleClick, handleBlur, handleFormat }) => {
var _a;
return /* @__PURE__ */ jsx("div", { style: { margin: 0, textAlign: ((_a = component.props.style) == null ? void 0 : _a.textAlign) || "left", ...component.props.containerStyle }, children: /* @__PURE__ */ jsxs("div", { className: "relative inline-block", onBlur: handleBlur, children: [
isEditing && /* @__PURE__ */ jsx(InlineToolbar, { onFormat: handleFormat }),
/* @__PURE__ */ jsx(
"div",
{
ref: contentRef,
onClick: handleClick,
contentEditable: isEditing,
suppressContentEditableWarning: true,
dir: "ltr",
role: "button",
"aria-label": "Editable button",
style: {
display: "inline-block",
padding: "12px 24px",
backgroundColor: component.props.backgroundColor || "#3b82f6",
color: component.props.color || "#ffffff",
textDecoration: "none",
fontWeight: "500",
outline: "none",
cursor: isEditing ? "text" : "pointer",
whiteSpace: "pre-wrap",
...component.props.style
},
dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(component.props.content || "Click me") }
}
)
] }) });
});
var ImageRenderer = React8.memo(({ component }) => /* @__PURE__ */ jsx("div", { style: { margin: 0, ...component.props.containerStyle }, children: /* @__PURE__ */ jsx(
"img",
{
src: component.props.src || "https://via.placeholder.com/600x300",
alt: component.props.alt || "Image",
style: {
maxWidth: "100%",
height: "auto",
...component.props.style
}
}
) }));
var DividerRenderer = React8.memo(({ component }) => /* @__PURE__ */ jsx(
"hr",
{
style: {
margin: 0,
border: "none",
borderTop: `1px solid ${component.props.color || "#e5e7eb"}`,
...component.props.style
}
}
));
var SpacerRenderer = React8.memo(({ component, isSelected }) => /* @__PURE__ */ jsx(
"div",
{
style: {
height: component.props.height || "24px",
backgroundColor: isSelected ? "rgba(59, 130, 246, 0.1)" : "transparent",
...component.props.style
}
}
));
var SocialRenderer = React8.memo(({ component }) => {
var _a;
return /* @__PURE__ */ jsx("div", { style: {
display: "flex",
gap: "12px",
justifyContent: ((_a = component.props.style) == null ? void 0 : _a.justifyContent) || "center",
...component.props.style
}, children: (component.props.networks || []).map((network, i) => /* @__PURE__ */ jsx(
"a",
{
href: network.href,
"aria-label": `Visit our ${network.name} page`,
style: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: "32px",
height: "32px",
borderRadius: "50%",
backgroundColor: "#e5e7eb",
color: "#374151",
textDecoration: "none",
fontSize: "14px",
fontWeight: "bold"
},
onClick: (e) => e.preventDefault(),
children: network.name[0].toUpperCase()
},
i
)) });
});
function ComponentRenderer({ component, onUpdateContent, onSelect, isSelected, selectedId }) {
const { type, props } = component;
const [isEditing, setIsEditing] = React8.useState(false);
const contentRef = React8.useRef(null);
const handleBlur = (e) => {
if (e.currentTarget.contains(e.relatedTarget)) {
return;
}
setIsEditing(false);
if (contentRef.current) {
const finalContent = contentRef.current.innerHTML || "";
const sanitizedContent = DOMPurify.sanitize(finalContent);
onUpdateContent(component.id, sanitizedContent);
}
};
const handleFormat = (command, value) => {
document.execCommand(command, false, value);
if (contentRef.current) {
const finalContent = contentRef.current.innerHTML || "";
const sanitizedContent = DOMPurify.sanitize(finalContent);
onUpdateContent(component.id, sanitizedContent);
}
};
const handleClick = (e) => {
e.stopPropagation();
if (!isEditing && (type === "text" || type === "heading" || type === "button")) {
setIsEditing(true);
setTimeout(() => {
if (contentRef.current) {
contentRef.current.focus();
}
}, 0);
}
};
if (type === "grid") {
const columns = component.children || [];
return /* @__PURE__ */ jsx(
"div",
{
style: {
margin: 0,
border: isSelected ? "2px dashed #3b82f6" : "2px dashed transparent",
padding: "8px",
...props.style
},
children: /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "16px" }, children: columns.map((column, index) => /* @__PURE__ */ jsx(
GridColumn,
{
column,
index,
isSelected: selectedId === column.id,
selectedId,
onSelect,
onUpdateContent
},
column.id
)) })
}
);
}
if (["container", "section", "header", "footer", "hero", "features"].includes(type)) {
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
id: `${component.id}-dropzone`,
data: { parentId: component.id }
});
return /* @__PURE__ */ jsxs(
"div",
{
ref: setDroppableRef,
style: {
margin: 0,
border: isSelected ? "2px dashed #3b82f6" : isOver ? "2px dashed #3b82f6" : "2px dashed transparent",
backgroundColor: isOver ? "rgba(59, 130, 246, 0.05)" : void 0,
minHeight: "50px",
padding: "10px",
position: "relative",
...props.style
},
children: [
isOver && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-blue-500/10 z-20 pointer-events-none rounded", children: /* @__PURE__ */ jsx("div", { className: "bg-blue-500 text-white px-3 py-1 rounded-full text-sm font-medium shadow-sm", children: "Drop here" }) }),
!component.children || component.children.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center h-20 text-xs text-muted-foreground dark:text-neutral-400 border border-dashed border-neutral-300 dark:border-neutral-700 rounded", children: [
/* @__PURE__ */ jsx(PlusIcon, { className: "size-3 mr-1" }),
"Drop components here"
] }) : /* @__PURE__ */ jsx(
SortableContext,
{
items: component.children.map((c) => c.id),
strategy: verticalListSortingStrategy,
children: component.children.map((child) => /* @__PURE__ */ jsx(
SortableEmailComponent,
{
component: child,
isSelected: selectedId === child.id,
selectedId,
onSelect,
onUpdateContent
},
child.id
))
}
)
]
}
);
}
switch (type) {
case "text":
return /* @__PURE__ */ jsx(TextRenderer, { component, isEditing, contentRef, handleClick, handleBlur, handleFormat });
case "heading":
return /* @__PURE__ */ jsx(HeadingRenderer, { component, isEditing, contentRef, handleClick, handleBlur, handleFormat });
case "button":
return /* @__PURE__ */ jsx(ButtonRenderer, { component, isEditing, contentRef, handleClick, handleBlur, handleFormat });
case "image":
return /* @__PURE__ */ jsx(ImageRenderer, { component });
case "divider":
return /* @__PURE__ */ jsx(DividerRenderer, { component });
case "spacer":
return /* @__PURE__ */ jsx(SpacerRenderer, { component, isSelected });
case "social":
return /* @__PURE__ */ jsx(SocialRenderer, { component });
default:
return null;
}
}
function EmailPreview({ components, selectedId, onSelect, onUpdateContent, viewMode = "desktop" }) {
const { setNodeRef, isOver } = useDroppable({
id: "email-canvas"
});
return /* @__PURE__ */ jsx("div", { className: "flex-1 bg-muted/20 p-8 overflow-y-auto flex justify-center items-start", onClick: () => onSelect(""), children: /* @__PURE__ */ jsxs(
"div",
{
ref: setNodeRef,
className: `
bg-white shadow-sm min-h-[800px] transition-all duration-300 text-black relative
${viewMode === "mobile" ? "w-[375px] rounded-3xl border-8 border-neutral-800" : "w-[600px]"}
${isOver ? "ring-4 ring-blue-500/50" : ""}
`,
style: {
fontFamily: "Arial, sans-serif"
},
children: [
isOver && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-blue-500/10 z-20 pointer-events-none rounded-lg", children: /* @__PURE__ */ jsx("div", { className: "bg-blue-500 text-white px-4 py-2 rounded-full text-base font-medium shadow-lg", children: "Drop component here" }) }),
components.length === 0 ? /* @__PURE__ */ jsx("div", { className: "h-full flex flex-col items-center justify-center text-muted-foreground p-8 border-2 border-dashed border-muted m-4 rounded-lg", children: /* @__PURE__ */ jsx("p", { children: "Drag and drop components here" }) }) : /* @__PURE__ */ jsx(
SortableContext,
{
items: components.map((c) => c.id),
strategy: verticalListSortingStrategy,
children: components.map((component) => /* @__PURE__ */ jsx(
SortableEmailComponent,
{
component,
isSelected: selectedId === component.id,
selectedId: selectedId || void 0,
onSelect,
onUpdateContent
},
component.id
))
}
)
]
}
) });
}
function Input({ className, type, ...props }) {
return /* @__PURE__ */ jsx(
"input",
{
type,
"data-slot": "input",
className: cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
),
...props
}
);
}
function Label({
className,
...props
}) {
return /* @__PURE__ */ jsx(
LabelPrimitive.Root,
{
"data-slot": "label",
className: cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
),
...props
}
);
}
function Textarea({ className, ...props }) {
return /* @__PURE__ */ jsx(
"textarea",
{
"data-slot": "textarea",
className: cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
),
...props
}
);
}
function TooltipProvider({
delayDuration = 0,
...props
}) {
return /* @__PURE__ */ jsx(
TooltipPrimitive.Provider,
{
"data-slot": "tooltip-provider",
delayDuration,
...props
}
);
}
function Tooltip({
...props
}) {
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(TooltipPrimitive.Root, { "data-slot": "tooltip", ...props }) });
}
function TooltipTrigger({
...props
}) {
return /* @__PURE__ */ jsx(TooltipPrimitive.Trigger, { "data-slot": "tooltip-trigger", ...props });
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}) {
return /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
TooltipPrimitive.Content,
{
"data-slot": "tooltip-content",
sideOffset,
className: cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
),
...props,
children: [
children,
/* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { className: "bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" })
]
}
) });
}
function CSSPropertyInput({ label, value, onChange, allowIndividual = false, sides }) {
const [isExpanded, setIsExpanded] = React8.useState(false);
const [unit, setUnit] = React8.useState("px");
const parseValue = (val) => {
if (!val || val === "auto") return { num: "", unit: "auto" };
const match = val.match(/^([\d.]+)(.*)$/);
if (match) {
return { num: match[1], unit: match[2] || "px" };
}
return { num: "", unit: "px" };
};
const parseIndividualValues = (val) => {
if (!val) return {};
const parts = val.split(" ").filter(Boolean);
if (!sides) return {};
if (parts.length === 1) {
return sides.reduce((acc, side) => ({ ...acc, [side]: parts[0] }), {});
} else if (parts.length === 2) {
return {
[sides[0]]: parts[0],
[sides[1]]: parts[1],
[sides[2]]: parts[0],
[sides[3]]: parts[1]
};
} else if (parts.length === 4) {
return {
[sides[0]]: parts[0],
[sides[1]]: parts[1],
[sides[2]]: parts[2],
[sides[3]]: parts[3]
};
}
return {};
};
const [individualValues, setIndividualValues] = React8.useState(
() => parseIndividualValues(value)
);
const handleExpand = React8.useCallback(() => {
setIsExpanded(true);
setIndividualValues(parseIndividualValues(value));
}, [value]);
React8.useEffect(() => {
const parsed = parseValue(value);
setUnit(parsed.unit);
}, [value]);
const handleValueChange = (newValue) => {
if (unit === "auto") {
onChange("auto");
} else {
onChange(newValue ? `${newValue}${unit}` : "");
}
};
const handleUnitChange = (newUnit) => {
setUnit(newUnit);
if (newUnit === "auto") {
onChange("auto");
} else {
const parsed = parseValue(value);
if (parsed.num) {
onChange(`${parsed.num}${newUnit}`);
}
}
};
const handleIndividualChange = (side, val) => {
const newValues = { ...individualValues, [side]: val };
setIndividualValues(newValues);
if (!sides) return;
const values = sides.map((s) => newValues[s] || "0px");
if (values.every((v) => v === values[0])) {
onChange(values[0]);
} else {
onChange(values.join(" "));
}
};
const currentValue = parseValue(value);
if (isExpanded && allowIndividual && sides) {
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: label }),
/* @__PURE__ */ jsxs(Tooltip, { children: [
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
onClick: () => setIsExpanded(false),
className: "h-6 px-2 cursor-pointer",
children: /* @__PURE__ */ jsx(UnfoldHorizontalIcon, { className: "size-3" })
}
) }),
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Collapse to single value" }) })
] })
] }),
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: sides.map((side) => {
const sideValue = parseValue(individualValues[side] || "0px");
return /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
/* @__PURE__ */ jsx(
Input,
{
type: "number",
value: sideValue.num,
onChange: (e) => handleIndividualChange(side, e.target.value ? `${e.target.value}${sideValue.unit}` : ""),
placeholder: "0",
className: "h-8 text-xs"
}
),
/* @__PURE__ */ jsxs(
"select",
{
value: sideValue.unit,
onChange: (e) => handleIndividualChange(side, sideValue.num ? `${sideValue.num}${e.target.value}` : ""),
className: "h-8 w-16 rounded-md border border-input bg-background px-2 text-xs",
children: [
/* @__PURE__ */ jsx("option", { value: "px", children: "px" }),
/* @__PURE__ */ jsx("option", { value: "%", children: "%" }),
/* @__PURE__ */ jsx("option", { value: "em", children: "em" }),
/* @__PURE__ */ jsx("option", { value: "rem", children: "rem" })
]
}
)
] }, side);
}) }),
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-1 mt-1 text-[10px] text-muted-foreground", children: sides.map((side) => /* @__PURE__ */ jsx("div", { className: "text-center capitalize", children: side.replace("-", " ") }, side)) })
] }) });
}
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: label }),
allowIndividual && sides && /* @__PURE__ */ jsxs(Tooltip, { children: [
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
onClick: handleExpand,
className: "h-6 px-2 cursor-pointer",
children: /* @__PURE__ */ jsx(UnfoldHorizontalIcon, { className: "size-3" })
}
) }),
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Set individual values" }) })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
/* @__PURE__ */ jsx(
Input,
{
type: unit === "auto" ? "text" : "number",
value: unit === "auto" ? "auto" : currentValue.num,
onChange: (e) => handleValueChange(e.target.value),
placeholder: unit === "auto" ? "auto" : "0",
disabled: unit === "auto",
className: "flex-1"
}
),
/* @__PURE__ */ jsxs(
"select",
{
value: unit,
onChange: (e) => handleUnitChange(e.target.value),
className: "w-20 rounded-md border border-input bg-background px-2 text-sm",
children: [
/* @__PURE__ */ jsx("option", { value: "px", children: "px" }),
/* @__PURE__ */ jsx("option", { value: "%", children: "%" }),
/* @__PURE__ */ jsx("option", { value: "em", children: "em" }),
/* @__PURE__ */ jsx("option", { value: "rem", children: "rem" }),
/* @__PURE__ */ jsx("option", { value: "auto", children: "auto" })
]
}
)
] })
] }) });
}
var Dialog = DialogPrimitive.Root;
var DialogPortal = DialogPrimitive.Portal;
var DialogOverlay = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
DialogPrimitive.Overlay,
{
ref,
className: cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
),
...props
}
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
var DialogContent = React8.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
/* @__PURE__ */ jsx(DialogOverlay, {}),
/* @__PURE__ */ jsxs(
DialogPrimitive.Content,
{
ref,
className: cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
),
...props,
children: [
children,
/* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground", children: [
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
] })
]
}
)
] }));
DialogContent.displayName = DialogPrimitive.Content.displayName;
var DialogHeader = ({
className,
...props
}) => /* @__PURE__ */ jsx(
"div",
{
className: cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
),
...props
}
);
DialogHeader.displayName = "DialogHeader";
var DialogFooter = ({
className,
...props
}) => /* @__PURE__ */ jsx(
"div",
{
className: cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
),
...props
}
);
DialogFooter.displayName = "DialogFooter";
var DialogTitle = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
DialogPrimitive.Title,
{
ref,
className: cn(
"text-lg font-semibold leading-none tracking-tight",
className
),
...props
}
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
var DialogDescription = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
DialogPrimitive.Description,
{
ref,
className: cn("text-sm text-muted-foreground", className),
...props
}
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
var Slider = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(
SliderPrimitive.Root,
{
ref,
className: cn(
"relative flex w-full touch-none select-none items-center",
className
),
...props,
children: [
/* @__PURE__ */ jsx(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsx(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
/* @__PURE__ */ jsx(SliderPrimitive.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
]
}
));
Slider.displayName = SliderPrimitive.Root.displayName;
function ImageUploader({ onUpload, imageUploadUrl, onUrlChange, currentUrl }) {
const [isOpen, setIsOpen] = React8.useState(false);
const [imageSrc, setImageSrc] = React8.useState(null);
const [crop, setCrop] = React8.useState({ x: 0, y: 0 });
const [zoom, setZoom] = React8.useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = React8.useState(null);
const [isUploading, setIsUploading] = React8.useState(false);
const fileInputRef = React8.useRef(null);
const onFileChange = async (e) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const reader = new FileReader();
reader.addEventListener("load", () => {
var _a;
setImageSrc(((_a = reader.result) == null ? void 0 : _a.toString()) || null);
setIsOpen(true);
});
reader.readAsDataURL(file);
}
};
const onCropComplete = React8.useCallback((croppedArea, croppedAreaPixels2) => {
setCroppedAreaPixels(croppedAreaPixels2);
}, []);
const createImage = (url) => new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous");
image.src = url;
});
const getCroppedImg = async (imageSrc2, pixelCrop) => {
const image = await createImage(imageSrc2);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("No 2d context");
}
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height
);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Canvas is empty"));
return;
}
resolve(blob);
}, "image/jpeg");
});
};
const handleSave = async () => {
var _a;
if (!imageSrc || !croppedAreaPixels || !onUpload && !imageUploadUrl) return;
setIsUploading(true);
try {
const croppedBlob = await getCroppedImg(imageSrc, croppedAreaPixels);
const file = new File([croppedBlob], "cropped-image.jpg", { type: "image/jpeg" });
let url = "";
if (onUpload) {
url = await onUpload(file);
} else if (imageUploadUrl) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(imageUploadUrl, {
method: "POST",
body: formData
});
if (!response.ok) {
throw new Error("Upload failed");
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const data = await response.json();
url = data.url || data.secure_url || ((_a = data.data) == null ? void 0 : _a.url);
} else {
url = await response.text();
}
}
if (url) {
onUrlChange(url);
setIsOpen(false);
setImageSrc(null);
}
} catch (e) {
console.error(e);
} finally {
setIsUploading(false);
}
};
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxs(
Button,
{
variant: "outline",
className: "w-full",
onClick: () => {
var _a;
return (_a = fileInputRef.current) == null ? void 0 : _a.click();
},
disabled: !onUpload && !imageUploadUrl,
children: [
/* @__PURE__ */ jsx(UploadIcon, { className: "size-4 mr-2" }),
"Upload Image"
]
}
),
/* @__PURE__ */ jsx(
"input",
{
type: "file",
ref: fileInputRef,
onChange: onFileChange,
accept: "image/*",
className: "hidden"
}
)
] }),
/* @__PURE__ */ jsx(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[600px] max-h-[90vh] overflow-auto", children: [
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "Crop Image" }) }),
/* @__PURE__ */ jsx("div", { className: "relative h-[400px] w-full bg-neutral-900 rounded-md overflow-hidden", children: imageSrc && /* @__PURE__ */ jsx(
Cropper,
{
image: imageSrc,
crop,
zoom,
aspect: 4 / 3,
onCropChange: setCrop,
onCropComplete,
onZoomChange: setZoom
}
) }),
/* @__PURE__ */ jsxs("div", { className: "py-4", children: [
/* @__PURE__ */ jsx(Label, { children: "Zoom" }),
/* @__PURE__ */ jsx(
Slider,
{
value: [zoom],
min: 1,
max: 3,
step: 0.1,
onValueChange: (value) => setZoom(value[0]),
className: "mt-2"
}
)
] }),
/* @__PURE__ */ jsxs(DialogFooter, { children: [
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => setIsOpen(false), children: "Cancel" }),
/* @__PURE__ */ jsxs(Button, { onClick: handleSave, disabled: isUploading, children: [
isUploading && /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 mr-2 animate-spin" }),
"Save & Upload"
] })
] })
] }) })
] });
}
function PropertiesPanel({ component, onUpdate, onDelete, onUpload, imageUploadUrl }) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
const [isCollapsed, setIsCollapsed] = React8.useState(false);
const updateProp = (key, value) => {
if (!component) return;
onUpdate(component.id, {
props: { ...component.props, [key]: value }
});
};
const updateStyleProp = (key, value) => {
if (!component) return;
onUpdate(component.id, {
props: {
...component.props,
style: { ...component.props.style, [key]: value }
}
});
};
if (isCollapsed) {
return /* @__PURE__ */ jsx("div", { className: "border-l bg-muted/30 flex items-center justify-center", children: /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
onClick: () => setIsCollapsed(false),
className: "h-12",
children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4" })
}
) });
}
if (!component) {
return /* @__PURE__ */ jsxs("div", { className: "w-80 border-l bg-muted/30 p-4 relative", children: [
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "icon",
onClick: () => setIsCollapsed(true),
className: "absolute top-2 right-2 h-8 w-8",
children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-4" })
}
),
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select a component to edit its properties" })
] });
}
return /* @__PURE__ */ jsxs("div", { className: "w-80 border-l bg-muted/30 p-4 overflow-y-auto