UNPKG

resend-editor

Version:

A drag-and-drop email editor for React applications

1,232 lines (1,231 loc) 110 kB
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