@denarolabs/email-template 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/index.d.ts +425 -8
  2. package/dist/index.js +9841 -7
  3. package/package.json +16 -23
  4. package/dist/components/ai-email-editor/AIEmailEditor.d.ts +0 -3
  5. package/dist/components/ai-email-editor/AIEmailEditor.js +0 -207
  6. package/dist/components/ai-email-editor/chat-message.d.ts +0 -36
  7. package/dist/components/ai-email-editor/chat-message.js +0 -49
  8. package/dist/components/ai-email-editor/images.d.ts +0 -2
  9. package/dist/components/ai-email-editor/images.js +0 -14
  10. package/dist/components/ai-email-editor/model-picker.d.ts +0 -7
  11. package/dist/components/ai-email-editor/model-picker.js +0 -9
  12. package/dist/components/ai-email-editor/preview.d.ts +0 -35
  13. package/dist/components/ai-email-editor/preview.js +0 -728
  14. package/dist/components/ai-email-editor/send-test-email-modal.d.ts +0 -13
  15. package/dist/components/ai-email-editor/send-test-email-modal.js +0 -70
  16. package/dist/components/ai-email-editor/stream.d.ts +0 -20
  17. package/dist/components/ai-email-editor/stream.js +0 -57
  18. package/dist/components/ai-email-editor/use-ai-email-editor.d.ts +0 -117
  19. package/dist/components/ai-email-editor/use-ai-email-editor.js +0 -1308
  20. package/dist/components/ai-email-editor/view-html-modal.d.ts +0 -9
  21. package/dist/components/ai-email-editor/view-html-modal.js +0 -37
  22. package/dist/lib/ai-stream-contract.d.ts +0 -99
  23. package/dist/lib/ai-stream-contract.js +0 -35
  24. package/dist/lib/build-default-system-prompt.d.ts +0 -2
  25. package/dist/lib/build-default-system-prompt.js +0 -37
  26. package/dist/lib/capture-email-preview.d.ts +0 -5
  27. package/dist/lib/capture-email-preview.js +0 -73
  28. package/dist/lib/cn.d.ts +0 -2
  29. package/dist/lib/cn.js +0 -5
  30. package/dist/lib/merge-tag-validation.d.ts +0 -3
  31. package/dist/lib/merge-tag-validation.js +0 -45
  32. package/dist/lib/rasterize-image-client.d.ts +0 -4
  33. package/dist/lib/rasterize-image-client.js +0 -47
  34. package/dist/lib/strip-html-code-fences.d.ts +0 -1
  35. package/dist/lib/strip-html-code-fences.js +0 -6
  36. package/dist/schemas/aiEmail.d.ts +0 -224
  37. package/dist/schemas/aiEmail.js +0 -29
  38. package/dist/schemas/aiEmailResponse.d.ts +0 -15
  39. package/dist/schemas/aiEmailResponse.js +0 -15
  40. package/dist/types.d.ts +0 -57
  41. package/dist/types.js +0 -1
  42. package/dist/ui/button.d.ts +0 -11
  43. package/dist/ui/button.js +0 -34
  44. package/dist/ui/dialog.d.ts +0 -33
  45. package/dist/ui/dialog.js +0 -39
  46. package/dist/ui/dropdown-menu.d.ts +0 -8
  47. package/dist/ui/dropdown-menu.js +0 -23
  48. package/dist/ui/input.d.ts +0 -2
  49. package/dist/ui/input.js +0 -6
  50. package/dist/ui/textarea.d.ts +0 -2
  51. package/dist/ui/textarea.js +0 -7
  52. package/src/styles.css +0 -197
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denarolabs/email-template",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "AI-powered email template editor React component",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,25 +11,22 @@
11
11
  "access": "public"
12
12
  },
13
13
  "type": "module",
14
- "sideEffects": [
15
- "*.css"
16
- ],
14
+ "sideEffects": true,
17
15
  "main": "./dist/index.js",
18
16
  "types": "./dist/index.d.ts",
19
17
  "exports": {
20
18
  ".": {
21
19
  "types": "./dist/index.d.ts",
22
20
  "import": "./dist/index.js"
23
- },
24
- "./styles.css": "./src/styles.css"
21
+ }
25
22
  },
26
23
  "files": [
27
- "dist",
28
- "src/styles.css"
24
+ "dist"
29
25
  ],
30
26
  "scripts": {
31
- "clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
32
- "build": "npm run clean && tsc -p tsconfig.package.json",
27
+ "clean": "node --eval \"const fs=require('node:fs'); for (const dir of ['dist','.build']) fs.rmSync(dir,{recursive:true,force:true})\"",
28
+ "build:css": "node --eval \"require('node:fs').mkdirSync('.build',{recursive:true})\" && npx @tailwindcss/cli -i ./src/styles.css -o ./.build/styles.compiled.css",
29
+ "build": "npm run clean && npm run build:css && tsup",
33
30
  "build:package": "npm run build",
34
31
  "prepublishOnly": "npm run build",
35
32
  "typecheck": "tsc -p tsconfig.package.json --noEmit && tsc -p tsconfig.json --noEmit",
@@ -37,38 +34,34 @@
37
34
  "build:app": "next build",
38
35
  "start": "next start"
39
36
  },
40
- "dependencies": {
41
- "@radix-ui/react-dropdown-menu": "^2.1.18",
42
- "@radix-ui/react-slot": "^1.3.0",
43
- "class-variance-authority": "^0.7.1",
44
- "clsx": "^2.1.1",
45
- "html-to-image": "^1.11.13",
46
- "lucide-react": "^0.511.0",
47
- "react-icons": "^5.5.0",
48
- "tailwind-merge": "^3.3.0",
49
- "zod": "^3.24.2"
50
- },
51
37
  "peerDependencies": {
52
38
  "ai": "^6.0.0",
53
39
  "react": "^18.0.0 || ^19.0.0",
54
- "react-dom": "^18.0.0 || ^19.0.0"
40
+ "react-dom": "^18.0.0 || ^19.0.0",
41
+ "zod": "^3.24.0"
55
42
  },
56
43
  "devDependencies": {
57
44
  "@ai-sdk/google": "^3.0.83",
58
45
  "@ai-sdk/openai": "^3.0.72",
59
46
  "@aws-sdk/client-dynamodb": "^3.1071.0",
60
47
  "@aws-sdk/lib-dynamodb": "^3.1071.0",
48
+ "@tailwindcss/cli": "^4.3.1",
61
49
  "@tailwindcss/postcss": "^4.3.1",
62
50
  "@types/node": "^25.9.3",
63
51
  "@types/react": "^19.2.17",
64
52
  "@types/react-dom": "^19.2.3",
65
53
  "ai": "^6.0.207",
54
+ "clsx": "^2.1.1",
55
+ "html-to-image": "^1.11.13",
66
56
  "next": "^16.2.9",
67
57
  "postcss": "^8.5.15",
68
58
  "react": "^19.2.7",
69
59
  "react-dom": "^19.2.7",
70
60
  "sharp": "^0.35.1",
61
+ "tailwind-merge": "^3.3.0",
71
62
  "tailwindcss": "^4.3.1",
72
- "typescript": "^6.0.3"
63
+ "tsup": "^8.5.0",
64
+ "typescript": "^6.0.3",
65
+ "zod": "^3.24.2"
73
66
  }
74
67
  }
@@ -1,3 +0,0 @@
1
- import type { AIEmailEditorProps } from "../../types";
2
- export declare function AIEmailEditor({ name, savedTemplate, onSave, streamRoute, mergeTags, defaultMergeTagValues, mode, systemPrompt, onUploadImage, onDeleteImage, onSendTestEmail, defaultTestEmailRecipient, defaultTestEmailSubject, onError, onSuccess, open, onOpenChange, brandLabel, brandIconUrl, productLabel, models, defaultModel, initialSuggestions, }: AIEmailEditorProps): import("react").JSX.Element | null;
3
- export default AIEmailEditor;
@@ -1,207 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { ArrowUp, Code2, Lightbulb, Loader2, Send, Maximize2, Minimize2, Paperclip, RotateCcw, Save, SlidersHorizontal, Square, Undo2, X, } from "lucide-react";
5
- import { TbClick } from "react-icons/tb";
6
- import { Button } from "../../ui/button";
7
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogTitle, } from "../../ui/dialog";
8
- import { Textarea } from "../../ui/textarea";
9
- import { ChatMessage } from "./chat-message";
10
- import { ModelPicker } from "./model-picker";
11
- import { SendTestEmailModal } from "./send-test-email-modal";
12
- import { useAiEmailEditor } from "./use-ai-email-editor";
13
- import { ViewHtmlModal } from "./view-html-modal";
14
- const CHAT_PANEL_DEFAULT_WIDTH = 532;
15
- const CHAT_PANEL_MIN_WIDTH = 320;
16
- const TEMPLATE_PANEL_MIN_WIDTH = 360;
17
- export function AIEmailEditor({ name, savedTemplate, onSave, streamRoute, mergeTags, defaultMergeTagValues, mode = "email", systemPrompt, onUploadImage, onDeleteImage, onSendTestEmail, defaultTestEmailRecipient, defaultTestEmailSubject, onError, onSuccess, open = true, onOpenChange, brandLabel, brandIconUrl, productLabel = "Email Designer", models, defaultModel, initialSuggestions, }) {
18
- const editor = useAiEmailEditor({
19
- name,
20
- savedTemplate,
21
- onSave,
22
- streamRoute,
23
- mergeTags,
24
- mode,
25
- systemPrompt,
26
- onUploadImage,
27
- onDeleteImage,
28
- onError,
29
- onSuccess,
30
- open,
31
- models,
32
- defaultModel,
33
- initialSuggestions,
34
- });
35
- const [restartOpen, setRestartOpen] = useState(false);
36
- const [htmlViewOpen, setHtmlViewOpen] = useState(false);
37
- const [testEmailOpen, setTestEmailOpen] = useState(false);
38
- const [advancedOpen, setAdvancedOpen] = useState(false);
39
- const [systemPromptDraft, setSystemPromptDraft] = useState("");
40
- const [mobileView, setMobileView] = useState("chat");
41
- const [suggestionsCollapsed, setSuggestionsCollapsed] = useState(false);
42
- const [chatPanelWidth, setChatPanelWidth] = useState(CHAT_PANEL_DEFAULT_WIDTH);
43
- const chatInputRef = useRef(null);
44
- const fileInputRef = useRef(null);
45
- const splitContainerRef = useRef(null);
46
- const [prevHasHtml, setPrevHasHtml] = useState(editor.hasHtml);
47
- if (editor.hasHtml !== prevHasHtml) {
48
- setPrevHasHtml(editor.hasHtml);
49
- if (editor.hasHtml)
50
- setMobileView("template");
51
- }
52
- useEffect(() => {
53
- if (!open)
54
- return;
55
- const htmlEl = document.documentElement;
56
- const body = document.body;
57
- const prev = {
58
- htmlOverflow: htmlEl.style.overflow,
59
- bodyOverflow: body.style.overflow,
60
- };
61
- htmlEl.style.overflow = "hidden";
62
- body.style.overflow = "hidden";
63
- return () => {
64
- htmlEl.style.overflow = prev.htmlOverflow;
65
- body.style.overflow = prev.bodyOverflow;
66
- };
67
- }, [open]);
68
- const chatPlaceholder = useMemo(() => {
69
- if (editor.contextImages.length > 0) {
70
- return "Describe what you want, e.g. “Make a template like this”…";
71
- }
72
- if (editor.hasHtml) {
73
- return "Ask a follow-up… (paste or drop a reference image)";
74
- }
75
- return "Describe the email you want… (paste or drop a reference image)";
76
- }, [editor.contextImages.length, editor.hasHtml]);
77
- useEffect(() => {
78
- const el = chatInputRef.current;
79
- if (!el)
80
- return;
81
- const minHeight = 48;
82
- const maxHeight = 128;
83
- el.style.height = "auto";
84
- const nextHeight = Math.max(minHeight, Math.min(el.scrollHeight, maxHeight));
85
- el.style.height = `${nextHeight}px`;
86
- el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
87
- }, [editor.input]);
88
- const handleSaveAndClose = async () => {
89
- const saved = await editor.handleSave();
90
- if (!saved)
91
- return;
92
- onOpenChange?.(false);
93
- };
94
- const inputDisabled = editor.isStreaming || editor.contextImageBusy;
95
- const canSend = !inputDisabled && (!!editor.input.trim() || editor.contextImages.length > 0);
96
- const clampChatPanelWidth = useCallback((width) => {
97
- const containerWidth = splitContainerRef.current?.getBoundingClientRect().width ?? window.innerWidth;
98
- const maxWidth = Math.max(CHAT_PANEL_MIN_WIDTH, containerWidth - TEMPLATE_PANEL_MIN_WIDTH);
99
- return Math.max(CHAT_PANEL_MIN_WIDTH, Math.min(maxWidth, width));
100
- }, []);
101
- const handleChatResizePointerDown = useCallback((event) => {
102
- if (event.button !== 0)
103
- return;
104
- event.preventDefault();
105
- const startX = event.clientX;
106
- const startWidth = chatPanelWidth;
107
- const onPointerMove = (moveEvent) => {
108
- setChatPanelWidth(clampChatPanelWidth(startWidth + moveEvent.clientX - startX));
109
- };
110
- const onPointerUp = () => {
111
- document.removeEventListener("pointermove", onPointerMove);
112
- document.removeEventListener("pointerup", onPointerUp);
113
- document.body.style.cursor = "";
114
- document.body.style.userSelect = "";
115
- };
116
- document.body.style.cursor = "col-resize";
117
- document.body.style.userSelect = "none";
118
- document.addEventListener("pointermove", onPointerMove);
119
- document.addEventListener("pointerup", onPointerUp);
120
- }, [chatPanelWidth, clampChatPanelWidth]);
121
- const templateName = savedTemplate?.name ?? name;
122
- if (!open)
123
- return null;
124
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-background text-foreground", children: [_jsxs("header", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border px-3 sm:px-4", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2 text-sm", children: [brandIconUrl && (_jsx("img", { src: brandIconUrl, alt: "", className: "hidden h-5 w-5 shrink-0 rounded-full object-cover md:block" })), brandLabel && (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden truncate font-semibold md:inline", children: brandLabel }), _jsx("span", { className: "hidden shrink-0 text-muted-foreground/40 md:inline", children: "|" })] })), _jsx("span", { className: "hidden truncate text-muted-foreground md:inline", children: productLabel }), templateName && (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden shrink-0 text-muted-foreground/40 md:inline", children: "|" }), _jsx("span", { className: "truncate font-semibold", children: templateName })] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
125
- setSystemPromptDraft(editor.effectiveSystemPrompt);
126
- setAdvancedOpen(true);
127
- }, disabled: editor.loading, className: "h-7 gap-1 px-2 text-xs", title: "Adjust the AI system prompt for this session", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "hidden sm:inline", children: "Advanced" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setHtmlViewOpen(true), disabled: !editor.hasHtml || editor.loading, className: "h-7 gap-1 px-2 text-xs", children: [_jsx(Code2, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "hidden sm:inline", children: "View HTML" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setTestEmailOpen(true), disabled: !editor.hasHtml || editor.loading, className: "h-7 gap-1 px-2 text-xs", children: [_jsx(Send, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "hidden sm:inline", children: "Send test email" })] })] })] }), editor.loading ? (_jsxs("div", { className: "flex flex-1 items-center justify-center text-muted-foreground", children: [_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" }), "Loading template..."] })) : (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: [_jsx("div", { className: "flex shrink-0 border-b border-border px-3 py-2 md:hidden", children: _jsxs("div", { className: "flex w-full rounded-lg bg-muted p-1", children: [_jsx("button", { type: "button", onClick: () => setMobileView("chat"), className: `flex-1 rounded-md py-2 text-sm font-medium ${mobileView === "chat"
128
- ? "bg-background shadow-sm"
129
- : "text-muted-foreground"}`, children: "Chat" }), _jsx("button", { type: "button", onClick: () => setMobileView("template"), className: `flex-1 rounded-md py-2 text-sm font-medium ${mobileView === "template"
130
- ? "bg-background shadow-sm"
131
- : "text-muted-foreground"}`, children: "Template" })] }) }), _jsxs("div", { ref: splitContainerRef, className: "relative min-h-0 flex-1 overflow-hidden md:flex", children: [_jsxs("div", { style: {
132
- "--chat-panel-width": `${chatPanelWidth}px`,
133
- }, className: `relative isolate flex h-full min-h-0 shrink-0 flex-col bg-background contain-[layout_paint] md:w-(--chat-panel-width) md:border-r md:border-border ${mobileView === "chat"
134
- ? "absolute inset-0 z-10 flex w-full md:static"
135
- : "hidden md:flex"}`, children: [_jsx("button", { type: "button", "aria-label": "Resize chat panel", onPointerDown: handleChatResizePointerDown, className: "absolute top-0 right-0 z-30 hidden h-full w-3 translate-x-1/2 cursor-col-resize touch-none border-0 bg-transparent p-0 after:absolute after:inset-y-0 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-border hover:after:w-0.5 hover:after:bg-primary/40 active:after:bg-primary/60 md:block" }), _jsx("div", { ref: editor.scrollRef, className: "scrollbar-hidden flex min-h-0 flex-1 flex-col overflow-y-auto", children: editor.messages.length === 0 ? (_jsxs("div", { className: "mx-auto flex max-w-2xl flex-1 flex-col items-center justify-center gap-2 px-3 py-12 text-center text-sm text-muted-foreground", children: [_jsx("p", { children: "No messages yet." }), _jsx("p", { className: "text-xs", children: "Send a message to describe the email you want." })] })) : (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [editor.messages.map((message) => (_jsx(ChatMessage, { message: {
136
- ...message,
137
- contextImages: message.contextImages?.map((img) => ({
138
- url: img.url,
139
- label: img.name ?? img.url,
140
- })),
141
- }, brandIconUrl: brandIconUrl, isEditing: editor.editingMessageId === message.id, editDraft: editor.editDraft, editContextImages: editor.editContextImages, contextImageBusy: editor.contextImageBusy, canEdit: !editor.isStreaming &&
142
- message.role === "user", canRevert: message.role === "user" &&
143
- editor.canRevertToCheckpoint(message.id), canRemoveContextImages: editor.canRemoveContextImages &&
144
- message.role === "user" &&
145
- !message.pending, onEditDraftChange: editor.setEditDraft, onStartEdit: () => editor.startEditing(message.id), onCancelEdit: editor.cancelEditing, onSubmitEdit: editor.requestSubmitEdit, onRevertToCheckpoint: () => editor.requestRevertToCheckpoint(message.id), onRemoveContextImage: (url) => void editor.removeMessageContextImage(message.id, url), onAddEditContextImage: editor.addEditContextImageFromFile, onRemoveEditContextImage: editor.removeEditContextImage }, message.id))), editor.chatPinSpacerHeight > 0 && (_jsx("div", { "aria-hidden": true, className: "shrink-0", style: {
146
- height: editor.chatPinSpacerHeight,
147
- } }))] })) }), _jsx("div", { className: "shrink-0 overflow-visible", children: _jsx("div", { className: "flex flex-col gap-1.5 bg-background px-4 py-3", children: _jsxs("div", { className: "mx-auto flex w-full max-w-2xl flex-col gap-1.5 overflow-visible", children: [editor.selectedBlockIds.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5", children: editor.selectedBlockIds.map((id) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-600", children: [id, _jsx("button", { type: "button", onClick: () => editor.toggleBlockSelection(id), children: _jsx(X, { className: "h-3 w-3" }) })] }, id))) })), editor.suggestions.length > 0 && (_jsx("div", { className: `grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none ${suggestionsCollapsed
148
- ? "grid-rows-[0fr] opacity-0"
149
- : "grid-rows-[1fr] opacity-100"}`, "aria-hidden": suggestionsCollapsed, children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsxs("div", { className: "max-h-[min(18rem,45vh)] overflow-y-auto rounded-xl border border-border bg-background shadow-[0_2px_8px_rgba(0,0,0,0.08),0_8px_24px_rgba(0,0,0,0.06)]", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border/80 px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2 text-xs font-medium text-muted-foreground", children: [_jsx(Lightbulb, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate text-foreground/80", children: "Suggested Next Actions" })] }), _jsx("button", { type: "button", onClick: () => setSuggestionsCollapsed(true), className: "shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500/30 disabled:opacity-50", "aria-label": "Collapse suggestions", title: "Collapse suggestions", children: _jsx(Minimize2, { className: "size-3.5" }) })] }), _jsx("ul", { className: "divide-y divide-border/80", role: "listbox", "aria-label": "Suggested Next Actions", children: editor.suggestions.map((suggestion, index) => (_jsx("li", { children: _jsxs("button", { type: "button", role: "option", onClick: () => editor.setInput(suggestion), disabled: inputDisabled, className: "flex w-full items-start gap-3 px-3 py-2.5 text-left text-sm leading-snug text-foreground transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50", children: [_jsx("span", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[0.625rem] font-medium text-muted-foreground", "aria-hidden": true, children: index + 1 }), _jsx("span", { className: "min-w-0 flex-1", children: suggestion })] }) }, suggestion))) }), _jsx("p", { className: "border-t border-border/80 px-3 py-2 text-[0.625rem] text-muted-foreground", children: "Or describe something else in the box below" })] }) }) })), suggestionsCollapsed &&
150
- editor.suggestions.length > 0 && (_jsxs("button", { type: "button", onClick: () => setSuggestionsCollapsed(false), className: "inline-flex items-center gap-1.5 self-start rounded-md px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground", children: [_jsx(Maximize2, { className: "size-3.5" }), "Show suggestions"] })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
151
- const file = e.target.files?.[0];
152
- if (file)
153
- void editor.addContextImageFromFile(file);
154
- e.target.value = "";
155
- } }), _jsxs("div", { className: `rounded-xl border border-border bg-background shadow-[0_2px_8px_rgba(0,0,0,0.08),0_8px_24px_rgba(0,0,0,0.06)] ${editor.chatDropActive
156
- ? "ring-2 ring-cyan-500/30"
157
- : ""}`, onDragOver: editor.handleChatDragOver, onDragLeave: editor.handleChatDragLeave, onDrop: editor.handleChatDrop, children: [editor.contextImages.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 border-b border-border/80 px-3 pt-3", children: editor.contextImages.map((image) => (_jsxs("div", { className: "flex items-center gap-1.5 rounded-lg border border-border bg-muted/40 py-1 pl-1 pr-2", children: [_jsx("img", { src: image.url, alt: image.name ?? image.url, className: "h-10 w-10 rounded object-cover" }), _jsx("button", { type: "button", onClick: () => void editor.removeContextImage(image.url), disabled: inputDisabled, className: "text-muted-foreground hover:text-foreground", children: _jsx(X, { className: "h-3 w-3" }) })] }, image.url))) })), _jsx("textarea", { ref: chatInputRef, value: editor.input, onFocus: () => {
158
- if (editor.editingMessageId)
159
- editor.cancelEditing();
160
- }, onChange: (e) => {
161
- if (editor.editingMessageId)
162
- editor.cancelEditing();
163
- editor.setInput(e.target.value);
164
- }, onPaste: editor.handleChatPaste, onKeyDown: (e) => {
165
- if (e.key === "Enter" && !e.shiftKey) {
166
- e.preventDefault();
167
- if (canSend)
168
- editor.handleSend();
169
- }
170
- }, placeholder: chatPlaceholder, disabled: inputDisabled, rows: 2, className: "block min-h-[4.5rem] w-full resize-none border-0 bg-transparent px-3 pt-3 pb-1 text-sm leading-relaxed outline-none placeholder:text-muted-foreground/80 disabled:cursor-not-allowed disabled:opacity-50" }), _jsxs("div", { className: "relative flex items-center justify-between gap-2 px-2 pb-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [editor.showModelSelector && (_jsx(ModelPicker, { options: editor.modelOptions, value: editor.selectedModel, onChange: editor.setSelectedModel, disabled: editor.loading ||
171
- editor.isStreaming })), _jsxs(Button, { type: "button", variant: "ghost", size: "xs", disabled: inputDisabled, onClick: () => fileInputRef.current?.click(), title: "Attach reference image", "aria-label": "Attach file", className: "gap-1 px-2 text-muted-foreground", children: [_jsx(Paperclip, { className: "size-3.5" }), _jsx("span", { children: "Attach" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "xs", onClick: () => {
172
- editor.toggleSelectMode();
173
- if (!editor.selectMode) {
174
- setMobileView("template");
175
- }
176
- }, disabled: !editor.hasHtml ||
177
- editor.isStreaming ||
178
- inputDisabled, title: "Select blocks in preview", "aria-label": "Select blocks", className: `gap-1 px-2 ${editor.selectMode
179
- ? "bg-primary/10 font-medium text-primary"
180
- : "text-muted-foreground"}`, children: [_jsx(TbClick, { className: "size-3.5" }), _jsx("span", { children: "Select" })] })] }), _jsx("div", { className: "flex items-center gap-1", children: _jsx(Button, { type: "button", variant: "outline", size: "icon-xs", onClick: editor.isStreaming
181
- ? editor.cancelStream
182
- : editor.handleSend, disabled: editor.isStreaming ? false : !canSend, title: editor.isStreaming
183
- ? "Stop generating"
184
- : "Send message", "aria-label": editor.isStreaming
185
- ? "Stop generating"
186
- : "Send message", className: editor.isStreaming || canSend
187
- ? "border-primary/30 bg-primary/10 text-primary hover:bg-primary/15"
188
- : "text-muted-foreground", children: editor.isStreaming ? (_jsx(Square, { className: "size-4 fill-current" })) : (_jsx(ArrowUp, { className: "size-4" })) }) })] })] })] }) }) })] }), _jsxs("div", { className: `flex min-h-0 flex-1 flex-col bg-muted/20 p-3 sm:p-4 ${mobileView === "template"
189
- ? "absolute inset-0 z-10 flex w-full md:static"
190
- : "hidden md:flex"}`, children: [editor.selectMode && (_jsxs("div", { className: "mb-3 flex shrink-0 items-center gap-2 rounded-lg border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-700", children: [_jsx(TbClick, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "flex-1", children: "Click blocks to target for AI \u00B7 click a selected block again to edit text" }), _jsx("button", { type: "button", onClick: () => {
191
- editor.exitSelectMode();
192
- setMobileView("chat");
193
- }, className: "rounded-md px-2 py-1 text-xs font-medium hover:bg-amber-500/20", children: "Done" })] })), _jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: [editor.hasHtml ? (_jsx("iframe", { ref: editor.iframeRef, title: `AI email editor: ${name}`, className: "h-full w-full rounded-md border border-border bg-white shadow-sm", sandbox: "allow-same-origin" })) : (_jsx("div", { className: "flex h-full flex-col items-center justify-center rounded-md border border-dashed border-border bg-background px-6 text-center text-muted-foreground", children: _jsx("p", { className: "max-w-sm text-sm", children: "Your email template will appear here after you describe it in chat." }) })), editor.previewDropActive && (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-cyan-500 bg-cyan-500/10 text-sm font-medium text-cyan-600", children: "Drop image here" }))] })] })] })] })), _jsxs("div", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-t border-border px-3 sm:px-4", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => onOpenChange?.(false), className: "h-7 px-2.5 text-xs", children: "Close" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setRestartOpen(true), disabled: !editor.canRestart || editor.loading, className: "h-7 gap-1 px-2.5 text-xs", children: [_jsx(RotateCcw, { className: "h-3.5 w-3.5" }), "Restart"] })] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: editor.requestBack, disabled: !editor.canGoBack, className: "h-7 gap-1 px-2.5 text-xs", children: [_jsx(Undo2, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "hidden sm:inline", children: "Undo" })] }), _jsxs(Button, { size: "sm", onClick: () => void handleSaveAndClose(), disabled: editor.saving ||
194
- editor.loading ||
195
- (!editor.hasHtml && !editor.hasSessionChanges), className: "h-7 gap-1 px-2.5 text-xs", children: [editor.saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(Save, { className: "h-3.5 w-3.5" })), "Save"] })] })] })] }), _jsx(Dialog, { open: restartOpen, onOpenChange: setRestartOpen, children: _jsxs(DialogContent, { showCloseButton: true, onClose: () => setRestartOpen(false), children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Restart template?" }), _jsx(DialogDescription, { children: "Clears the preview and chat history. Your saved template is unchanged until you click Save." })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRestartOpen(false), children: "Cancel" }), _jsx(Button, { variant: "destructive", onClick: () => {
196
- setRestartOpen(false);
197
- void editor.handleRestart();
198
- }, children: "Restart" })] })] }) }), _jsx(Dialog, { open: advancedOpen, onOpenChange: setAdvancedOpen, children: _jsxs(DialogContent, { showCloseButton: true, onClose: () => setAdvancedOpen(false), className: "max-h-[85vh] max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Advanced \u2014 system prompt" }), _jsx(DialogDescription, { children: "Edit the instructions sent to the AI for this session only." })] }), _jsx(DialogPanel, { children: _jsx(Textarea, { value: systemPromptDraft, onChange: (e) => setSystemPromptDraft(e.target.value), rows: 18, spellCheck: false, className: "min-h-[360px] font-mono text-xs" }) }), _jsxs(DialogFooter, { className: "sm:justify-between", children: [_jsx(Button, { variant: "outline", onClick: () => {
199
- setSystemPromptDraft(editor.defaultSystemPrompt);
200
- editor.resetSystemPrompt();
201
- }, children: "Reset to default" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => setAdvancedOpen(false), children: "Cancel" }), _jsx(Button, { onClick: () => {
202
- if (editor.applySystemPrompt(systemPromptDraft)) {
203
- setAdvancedOpen(false);
204
- }
205
- }, disabled: !systemPromptDraft.trim(), children: "Apply for session" })] })] })] }) }), _jsx(Dialog, { open: editor.resubmitConfirmOpen, onOpenChange: (o) => !o && editor.cancelSubmitConfirm(), children: _jsxs(DialogContent, { showCloseButton: true, onClose: editor.cancelSubmitConfirm, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Revert and resend?" }), _jsx(DialogDescription, { children: "Send will revert the preview to this point and re-run your prompt." })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: editor.cancelSubmitConfirm, children: "Cancel" }), _jsx(Button, { onClick: editor.confirmSubmitEdit, children: "Send" })] })] }) }), _jsx(Dialog, { open: editor.backConfirmOpen, onOpenChange: (o) => !o && editor.cancelBack(), children: _jsxs(DialogContent, { showCloseButton: true, onClose: editor.cancelBack, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Go back?" }), _jsx(DialogDescription, { children: editor.backConfirmSummary })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: editor.cancelBack, children: "Cancel" }), _jsx(Button, { onClick: () => void editor.confirmBack(), children: "Go back" })] })] }) }), _jsx(Dialog, { open: editor.revertConfirmOpen, onOpenChange: (o) => !o && editor.cancelRevertConfirm(), children: _jsxs(DialogContent, { showCloseButton: true, onClose: editor.cancelRevertConfirm, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Revert to checkpoint?" }), _jsx(DialogDescription, { children: "The preview and chat will go back to how they were after this message." })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: editor.cancelRevertConfirm, children: "Cancel" }), _jsx(Button, { onClick: () => void editor.confirmRevertToCheckpoint(), children: "Revert" })] })] }) }), _jsx(ViewHtmlModal, { open: htmlViewOpen, onOpenChange: setHtmlViewOpen, getHtml: editor.getCleanHtml, downloadFileName: name, onError: onError, onSuccess: onSuccess }), _jsx(SendTestEmailModal, { open: testEmailOpen, onOpenChange: setTestEmailOpen, getHtml: editor.getCleanHtml, templateName: name, defaultMergeTagValues: defaultMergeTagValues, defaultRecipient: defaultTestEmailRecipient, defaultSubject: defaultTestEmailSubject, onSendTestEmail: onSendTestEmail, onError: onError, onSuccess: onSuccess })] }));
206
- }
207
- export default AIEmailEditor;
@@ -1,36 +0,0 @@
1
- export declare function ChatMessage({ message, isEditing, editDraft, editContextImages, contextImageBusy, canEdit, canRevert, canRemoveContextImages, onEditDraftChange, onStartEdit, onCancelEdit, onSubmitEdit, onRevertToCheckpoint, onRemoveContextImage, onAddEditContextImage, onRemoveEditContextImage, appliedLabel, brandIconUrl, }: {
2
- message: {
3
- id: string;
4
- role: string;
5
- content: string;
6
- pending?: boolean;
7
- error?: boolean;
8
- phase?: string;
9
- appliedHtml?: boolean;
10
- selectedBlockIds?: string[];
11
- contextImages?: {
12
- url: string;
13
- label: string;
14
- }[];
15
- };
16
- isEditing: boolean;
17
- editDraft: string;
18
- editContextImages?: {
19
- url: string;
20
- label: string;
21
- }[];
22
- contextImageBusy?: boolean;
23
- canEdit: boolean;
24
- canRevert: boolean;
25
- canRemoveContextImages?: boolean;
26
- onEditDraftChange: (value: string) => void;
27
- onStartEdit: () => void;
28
- onCancelEdit: () => void;
29
- onSubmitEdit: () => void;
30
- onRevertToCheckpoint: () => void;
31
- onRemoveContextImage?: (url: string) => void;
32
- onAddEditContextImage?: (file: File) => void;
33
- onRemoveEditContextImage?: (url: string) => void;
34
- appliedLabel?: string;
35
- brandIconUrl?: string;
36
- }): import("react").JSX.Element;
@@ -1,49 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ImagePlus, ArrowUp, Check, CornerUpLeft, Loader2, X } from "lucide-react";
3
- import { useRef } from "react";
4
- import { Button } from "../../ui/button";
5
- import { Textarea } from "../../ui/textarea";
6
- import { phaseLabel } from "./stream";
7
- export function ChatMessage({ message, isEditing, editDraft, editContextImages, contextImageBusy, canEdit, canRevert, canRemoveContextImages, onEditDraftChange, onStartEdit, onCancelEdit, onSubmitEdit, onRevertToCheckpoint, onRemoveContextImage, onAddEditContextImage, onRemoveEditContextImage, appliedLabel, brandIconUrl, }) {
8
- const editFileInputRef = useRef(null);
9
- const renderContextImages = (images) => (_jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: images.map((image) => (_jsxs("div", { className: "group/image relative overflow-hidden rounded-lg border border-border bg-muted/40", title: image.label, children: [_jsx("img", { src: image.url, alt: image.label, className: "h-14 w-14 object-cover" }), canRemoveContextImages && onRemoveContextImage && (_jsx("button", { type: "button", onClick: (event) => {
10
- event.stopPropagation();
11
- onRemoveContextImage(image.url);
12
- }, className: "absolute right-0.5 top-0.5 rounded-full bg-background/90 p-0.5 text-muted-foreground shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground", title: "Remove reference image", children: _jsx(X, { className: "h-3 w-3" }) }))] }, image.url))) }));
13
- if (message.role === "user") {
14
- if (isEditing) {
15
- return (_jsx("div", { className: "flex min-w-0 justify-end", "data-message-id": message.id, children: _jsxs("div", { className: "w-full max-w-[85%] space-y-2", children: [!!editContextImages?.length && (_jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: editContextImages.map((image) => (_jsxs("div", { className: "group/image relative overflow-hidden rounded-lg border border-border bg-muted/40", title: image.label, children: [_jsx("img", { src: image.url, alt: image.label, className: "h-14 w-14 object-cover" }), onRemoveEditContextImage && (_jsx("button", { type: "button", onClick: () => onRemoveEditContextImage(image.url), className: "absolute right-0.5 top-0.5 rounded-full bg-background/90 p-0.5 text-muted-foreground shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground", title: "Remove image", children: _jsx(X, { className: "h-3 w-3" }) }))] }, image.url))) })), _jsx(Textarea, { value: editDraft, onChange: (e) => onEditDraftChange(e.target.value), onKeyDown: (e) => {
16
- if (e.key === "Enter" && !e.shiftKey) {
17
- e.preventDefault();
18
- onSubmitEdit();
19
- }
20
- else if (e.key === "Escape") {
21
- e.preventDefault();
22
- onCancelEdit();
23
- }
24
- }, autoFocus: true, className: "min-h-[72px] resize-none border-cyan-500/40 bg-background text-sm" }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "flex items-center gap-1", children: onAddEditContextImage && (_jsxs(_Fragment, { children: [_jsx("input", { ref: editFileInputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
25
- const file = e.target.files?.[0];
26
- if (file)
27
- onAddEditContextImage(file);
28
- e.target.value = "";
29
- } }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: contextImageBusy, onClick: () => editFileInputRef.current?.click(), className: "h-7 gap-1 px-2 text-xs text-muted-foreground", title: "Attach reference image", children: _jsx(ImagePlus, { className: "h-3 w-3" }) })] })) }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: onCancelEdit, className: "h-7 gap-1 px-2 text-xs", children: [_jsx(X, { className: "h-3 w-3" }), "Cancel"] }), _jsxs(Button, { size: "sm", onClick: onSubmitEdit, disabled: !editDraft.trim(), className: "h-7 gap-1 px-2 text-xs", children: [_jsx(ArrowUp, { className: "h-3 w-3" }), "Send"] })] })] })] }) }));
30
- }
31
- return (_jsx("div", { className: "flex min-w-0 justify-end", "data-message-id": message.id, children: _jsxs("div", { className: "w-full max-w-[85%] space-y-1", children: [!!message.contextImages?.length && renderContextImages(message.contextImages), !!message.selectedBlockIds?.length && (_jsx("div", { className: "flex flex-wrap justify-end gap-1", children: message.selectedBlockIds.map((id) => (_jsx("span", { className: "rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] text-amber-600", children: id }, id))) })), _jsxs("div", { className: `flex min-w-0 items-center gap-2 rounded-2xl rounded-br-sm border border-border bg-muted/60 px-3 py-2 text-sm text-foreground${canEdit ? " cursor-pointer hover:bg-muted/90" : ""}`, onClick: canEdit ? onStartEdit : undefined, role: canEdit ? "button" : undefined, tabIndex: canEdit ? 0 : undefined, onKeyDown: canEdit
32
- ? (e) => {
33
- if (e.key === "Enter" || e.key === " ")
34
- onStartEdit();
35
- }
36
- : undefined, children: [_jsx("span", { className: "min-w-0 flex-1 break-words [overflow-wrap:anywhere]", children: message.content }), canRevert && (_jsx("button", { type: "button", onClick: (e) => {
37
- e.stopPropagation();
38
- onRevertToCheckpoint();
39
- }, className: "shrink-0 rounded p-0.5 text-muted-foreground/70 transition-colors hover:text-foreground", title: "Revert to this checkpoint", children: _jsx(CornerUpLeft, { className: "h-3 w-3" }) }))] })] }) }));
40
- }
41
- const planLines = message.content
42
- .split("\n")
43
- .map((line) => line.trim())
44
- .filter((line) => line.startsWith("- "));
45
- const showsOnlyPhaseLabel = message.pending &&
46
- message.phase &&
47
- message.content === phaseLabel(message.phase);
48
- return (_jsxs("div", { className: "flex min-w-0 gap-2.5", "data-message-id": message.id, children: [brandIconUrl && (_jsx("div", { className: "mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full bg-cyan-500/10", children: _jsx("img", { src: brandIconUrl, alt: "", className: "h-5 w-5" }) })), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [message.pending && message.phase && (_jsxs("div", { className: "inline-flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), phaseLabel(message.phase)] })), !showsOnlyPhaseLabel && (_jsx("div", { className: `min-w-0 break-words text-sm leading-relaxed [overflow-wrap:anywhere] ${message.error ? "text-red-600" : "text-foreground"}`, children: planLines.length > 0 ? (_jsxs("ul", { className: "space-y-1", children: [planLines.map((line) => (_jsxs("li", { className: "flex min-w-0 gap-2", children: [_jsx("span", { className: "shrink-0 text-cyan-500", children: "\u2022" }), _jsx("span", { className: "min-w-0 flex-1 break-words [overflow-wrap:anywhere]", children: line.slice(2) })] }, line))), message.pending && message.phase === "applying" && (_jsxs("li", { className: "flex gap-2 text-muted-foreground", children: [_jsx(Loader2, { className: "mt-0.5 h-3 w-3 animate-spin" }), _jsx("span", { children: "Writing HTML\u2026" })] }))] })) : (_jsxs("span", { className: "whitespace-pre-wrap", children: [message.content, message.pending && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 translate-y-0.5 animate-pulse bg-cyan-500" }))] })) })), message.appliedHtml && !message.pending && (_jsxs("div", { className: "inline-flex items-center gap-1.5 rounded-md bg-emerald-500/10 px-2 py-1 text-xs font-medium text-emerald-600", children: [_jsx(Check, { className: "h-3 w-3" }), appliedLabel ?? "Applied to preview"] }))] })] }));
49
- }
@@ -1,2 +0,0 @@
1
- import type { AiEmailChatMessage, AiEmailImage } from "../../schemas/aiEmail";
2
- export declare function collectAllReferencedImageUrls(messages: Pick<AiEmailChatMessage, "role" | "contextImages">[], sessionImages: Pick<AiEmailImage, "url">[], pending?: Pick<AiEmailImage, "url">[]): Set<string>;
@@ -1,14 +0,0 @@
1
- export function collectAllReferencedImageUrls(messages, sessionImages, pending = []) {
2
- const urls = new Set();
3
- for (const msg of messages) {
4
- if (msg.role !== "user" || !msg.contextImages)
5
- continue;
6
- for (const img of msg.contextImages)
7
- urls.add(img.url);
8
- }
9
- for (const img of pending)
10
- urls.add(img.url);
11
- for (const img of sessionImages)
12
- urls.add(img.url);
13
- return urls;
14
- }
@@ -1,7 +0,0 @@
1
- import type { AIEmailModelOption } from "../../types";
2
- export declare function ModelPicker({ options, value, onChange, disabled, }: {
3
- options: readonly AIEmailModelOption[];
4
- value: string;
5
- onChange: (id: string) => void;
6
- disabled?: boolean;
7
- }): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { ChevronDown, Zap } from "lucide-react";
4
- import { Button } from "../../ui/button";
5
- import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "../../ui/dropdown-menu";
6
- export function ModelPicker({ options, value, onChange, disabled, }) {
7
- const selectedLabel = options.find((option) => option.id === value)?.label ?? value;
8
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "xs", disabled: disabled, className: "max-w-[11rem] gap-0.5 px-1.5 text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", "aria-label": `Model: ${selectedLabel}`, children: [_jsx(Zap, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: selectedLabel }), _jsx(ChevronDown, { className: "size-3 shrink-0" })] }) }), _jsxs(DropdownMenuContent, { side: "top", align: "start", className: "min-w-56", children: [_jsx(DropdownMenuLabel, { children: "Model" }), _jsx(DropdownMenuRadioGroup, { value: value, onValueChange: onChange, children: options.map((option) => (_jsx(DropdownMenuRadioItem, { value: option.id, className: "items-start py-2", children: _jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "font-medium text-foreground", children: option.label }), option.hint ? (_jsx("span", { className: "text-[0.6875rem] leading-snug text-muted-foreground", children: option.hint })) : null] }) }, option.id))) })] })] }));
9
- }
@@ -1,35 +0,0 @@
1
- export declare const AI_IMAGE_HANDLE_ID = "ai-email-image-handle";
2
- /** Visible size of the corner resize control (must match CSS). */
3
- export declare const AI_IMAGE_HANDLE_SIZE = 24;
4
- export declare const AI_IMAGE_EDITOR_STYLES = "\n img[data-ai-image] { cursor: grab; touch-action: none; }\n img[data-ai-image].ai-img-selected { cursor: grab; }\n img[data-ai-image].ai-img-dragging { cursor: grabbing; opacity: 0.25; }\n img[data-ai-image].ai-img-selected {\n outline: 2px solid rgb(6, 182, 212);\n outline-offset: 2px;\n }\n .ai-img-drag-ghost {\n position: fixed;\n margin: 0;\n padding: 0;\n pointer-events: none;\n z-index: 9998;\n opacity: 0.92;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.22);\n touch-action: none;\n }\n #ai-email-image-handle {\n position: fixed;\n width: 24px;\n height: 24px;\n margin: 0;\n padding: 0;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M12.5 3.5L3.5 12.5' stroke='%2306b6d4' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M12.5 3.5H9' stroke='%2306b6d4' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M12.5 3.5V7' stroke='%2306b6d4' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M3.5 12.5H7' stroke='%2306b6d4' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M3.5 12.5V9' stroke='%2306b6d4' stroke-width='1.75' stroke-linecap='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n border: 2px solid rgb(6, 182, 212);\n border-radius: 4px;\n cursor: nwse-resize;\n z-index: 10000;\n touch-action: none;\n box-shadow: 0 2px 8px rgba(15, 23, 42, 0.22);\n }\n #ai-email-image-handle:hover {\n background-color: rgb(6, 182, 212);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M12.5 3.5L3.5 12.5' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M12.5 3.5H9' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M12.5 3.5V7' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M3.5 12.5H7' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round'/%3E%3Cpath d='M3.5 12.5V9' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round'/%3E%3C/svg%3E\");\n }\n";
5
- /** Tag images and ensure they can be resized in the preview. */
6
- export declare function prepareImagesForEditing(doc: Document): void;
7
- export declare function sanitizeEmailHtml(html: string): string;
8
- export declare function stripImageEditorChrome(doc: Document): void;
9
- /** Unwrap centered-only wrapper divs so text can flow around the image. */
10
- export declare function unwrapCenteredImage(img: HTMLImageElement): void;
11
- export declare function attachImageResize(doc: Document, options: {
12
- isSelectMode: () => boolean;
13
- onBeforeEdit?: () => void;
14
- onResizeEnd: () => void;
15
- }): () => void;
16
- /** Float image for inline insert — text wraps in supporting clients and in preview. */
17
- export declare function createFloatImage(doc: Document, url: string, label: string, width?: number): HTMLImageElement;
18
- export declare const AI_EDITOR_STYLE_ID = "ai-email-editor-styles";
19
- export declare const AI_EDITOR_STYLES = "\n [data-edit-id] { transition: outline 0.15s ease, background 0.15s ease; }\n [data-edit-id].ai-selected {\n outline: 2px solid rgb(6, 182, 212);\n background: rgba(6, 182, 212, 0.07);\n }\n [data-edit-id][contenteditable=\"true\"] {\n outline: 2px solid rgb(34, 197, 94);\n outline-offset: 1px;\n cursor: text;\n }\n [data-edit-id][contenteditable=\"true\"]:not(button):not(a) {\n background: rgba(34, 197, 94, 0.06);\n }\n button[data-edit-id][contenteditable=\"true\"],\n a[data-edit-id][contenteditable=\"true\"] {\n outline: 2px dashed rgba(34, 197, 94, 0.85);\n outline-offset: 3px;\n cursor: text;\n }\n [data-edit-id][contenteditable=\"true\"]::selection,\n [data-edit-id][contenteditable=\"true\"] *::selection {\n background-color: rgba(6, 182, 212, 0.45);\n color: inherit;\n -webkit-text-fill-color: currentColor;\n text-shadow: none;\n }\n button[data-edit-id][contenteditable=\"true\"]::selection,\n button[data-edit-id][contenteditable=\"true\"] *::selection,\n a[data-edit-id][contenteditable=\"true\"]::selection,\n a[data-edit-id][contenteditable=\"true\"] *::selection {\n background-color: rgba(15, 23, 42, 0.82);\n color: #ffffff;\n -webkit-text-fill-color: #ffffff;\n text-shadow: none;\n }\n body.ai-select-mode [data-edit-id] { cursor: crosshair; }\n body.ai-select-mode [data-edit-id].ai-flagged { cursor: text; }\n body.ai-select-mode [data-edit-id]:hover:not(button):not(a) {\n outline: 2px dashed rgba(250, 204, 21, 0.85);\n }\n body.ai-select-mode button[data-edit-id]:hover,\n body.ai-select-mode a[data-edit-id]:hover {\n outline: 2px dashed rgba(250, 204, 21, 0.85);\n outline-offset: 2px;\n }\n body.ai-select-mode [data-edit-id].ai-flagged:not(button):not(a) {\n outline: 2px solid rgb(250, 204, 21);\n background: rgba(250, 204, 21, 0.14);\n box-shadow: inset 0 0 0 1px rgba(250, 204, 21, 0.35);\n }\n body.ai-select-mode button[data-edit-id].ai-flagged,\n body.ai-select-mode a[data-edit-id].ai-flagged {\n outline: 2px solid rgb(250, 204, 21);\n outline-offset: 2px;\n }\n body.ai-select-mode [data-edit-id].ai-flagged[contenteditable=\"true\"]:not(button):not(a) {\n outline: 2px solid rgb(34, 197, 94);\n background: rgba(34, 197, 94, 0.06);\n box-shadow: none;\n }\n body.ai-select-mode button[data-edit-id].ai-flagged[contenteditable=\"true\"],\n body.ai-select-mode a[data-edit-id].ai-flagged[contenteditable=\"true\"] {\n outline: 2px dashed rgba(34, 197, 94, 0.85);\n outline-offset: 3px;\n }\n";
20
- /** Tag text-bearing blocks so the editor can target them for click / AI edits. */
21
- export declare function annotateHtmlForEditing(html: string): string;
22
- export declare function serializeHtmlDocument(doc: Document): string;
23
- export declare function stripEditorChrome(html: string): string;
24
- /** Click-to-type: focus a block and place the caret where the user clicked. */
25
- export declare function focusEditableElementAtPoint(element: HTMLElement, clientX: number, clientY: number): void;
26
- export declare function finishInlineEdits(doc: Document, except?: Element): void;
27
- export declare function syncFlaggedElements(doc: Document, flaggedIds: Iterable<string>): void;
28
- export declare function getBlockHtmlById(doc: Document, editId: string): string | null;
29
- /** Appends an image block to the end of the email's main content. */
30
- export declare function insertImageBlock(doc: Document, url: string, label: string): void;
31
- /**
32
- * Inserts an image block right after the block under the given viewport point.
33
- * Falls back to appending at the end when the drop position is unclear.
34
- */
35
- export declare function insertImageBlockAtPoint(doc: Document, url: string, label: string, clientX: number, clientY: number): void;