@mjml-agent-editor/editor 0.1.0

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.
@@ -0,0 +1,1099 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/canvas.ts
32
+ var canvas_exports = {};
33
+ __export(canvas_exports, {
34
+ EmailEditor: () => EmailEditor
35
+ });
36
+ module.exports = __toCommonJS(canvas_exports);
37
+
38
+ // src/components/editor/EmailEditor.tsx
39
+ var import_grapesjs = __toESM(require("grapesjs"), 1);
40
+ var import_react4 = __toESM(require("@grapesjs/react"), 1);
41
+ var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
42
+ var import_react5 = require("react");
43
+
44
+ // src/stores.tsx
45
+ var import_react = require("react");
46
+ var import_jsx_runtime = require("react/jsx-runtime");
47
+ var StoreContext = (0, import_react.createContext)(null);
48
+ function useStores() {
49
+ const stores = (0, import_react.useContext)(StoreContext);
50
+ if (!stores) {
51
+ throw new Error(
52
+ "The editor needs an <EditorStoreProvider>. Wrap the editor and pass a DocumentStore and a CommentStore \u2014 see packages/editor/README.md."
53
+ );
54
+ }
55
+ return stores;
56
+ }
57
+ function useDocumentStore() {
58
+ return useStores().documents;
59
+ }
60
+ function useCommentStore() {
61
+ return useStores().comments;
62
+ }
63
+
64
+ // src/starter.ts
65
+ var STARTER_MJML = `<mjml>
66
+ <mj-body background-color="#f4f4f5">
67
+ <mj-section background-color="#ffffff" padding="32px 24px">
68
+ <mj-column>
69
+ <mj-text font-size="22px" font-weight="bold">New email</mj-text>
70
+ <mj-text color="#555555">
71
+ Describe the email you need in the chat on the right \u2014 the agent
72
+ will design the sections, content and images.
73
+ </mj-text>
74
+ </mj-column>
75
+ </mj-section>
76
+ </mj-body>
77
+ </mjml>`;
78
+
79
+ // src/lib/utils.ts
80
+ var import_clsx = require("clsx");
81
+ var import_tailwind_merge = require("tailwind-merge");
82
+ function cn(...inputs) {
83
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
84
+ }
85
+
86
+ // src/components/ui/button.tsx
87
+ var React = require("react");
88
+ var import_class_variance_authority = require("class-variance-authority");
89
+ var import_radix_ui = require("radix-ui");
90
+ var import_jsx_runtime2 = require("react/jsx-runtime");
91
+ var buttonVariants = (0, import_class_variance_authority.cva)(
92
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
93
+ {
94
+ variants: {
95
+ variant: {
96
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
97
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
98
+ secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
99
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
100
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
101
+ link: "text-primary underline-offset-4 hover:underline"
102
+ },
103
+ size: {
104
+ default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
105
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
106
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
107
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
108
+ icon: "size-8",
109
+ "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
110
+ "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
111
+ "icon-lg": "size-9"
112
+ }
113
+ },
114
+ defaultVariants: {
115
+ variant: "default",
116
+ size: "default"
117
+ }
118
+ }
119
+ );
120
+ function Button({
121
+ className,
122
+ variant = "default",
123
+ size = "default",
124
+ asChild = false,
125
+ ...props
126
+ }) {
127
+ const Comp = asChild ? import_radix_ui.Slot.Root : "button";
128
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
129
+ Comp,
130
+ {
131
+ "data-slot": "button",
132
+ "data-variant": variant,
133
+ "data-size": size,
134
+ className: cn(buttonVariants({ variant, size, className })),
135
+ ...props
136
+ }
137
+ );
138
+ }
139
+
140
+ // src/components/ui/input.tsx
141
+ var React2 = require("react");
142
+ var import_jsx_runtime3 = require("react/jsx-runtime");
143
+ function Input({ className, type, ...props }) {
144
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
145
+ "input",
146
+ {
147
+ type,
148
+ "data-slot": "input",
149
+ className: cn(
150
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
151
+ className
152
+ ),
153
+ ...props
154
+ }
155
+ );
156
+ }
157
+
158
+ // src/components/ui/spinner.tsx
159
+ var import_lucide_react = require("lucide-react");
160
+ var import_jsx_runtime4 = require("react/jsx-runtime");
161
+ function Spinner({ className, ...props }) {
162
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
163
+ import_lucide_react.Loader2Icon,
164
+ {
165
+ "data-slot": "spinner",
166
+ role: "status",
167
+ "aria-label": "Loading",
168
+ className: cn("size-4 animate-spin", className),
169
+ ...props
170
+ }
171
+ );
172
+ }
173
+
174
+ // src/components/ui/popover.tsx
175
+ var React3 = require("react");
176
+ var import_radix_ui2 = require("radix-ui");
177
+ var import_jsx_runtime5 = require("react/jsx-runtime");
178
+ function Popover({ ...props }) {
179
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Root, { "data-slot": "popover", ...props });
180
+ }
181
+ function PopoverAnchor({ ...props }) {
182
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Anchor, { "data-slot": "popover-anchor", ...props });
183
+ }
184
+ function PopoverContent({
185
+ className,
186
+ align = "center",
187
+ sideOffset = 4,
188
+ ...props
189
+ }) {
190
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
191
+ import_radix_ui2.Popover.Content,
192
+ {
193
+ "data-slot": "popover-content",
194
+ align,
195
+ sideOffset,
196
+ className: cn(
197
+ "bg-popover text-popover-foreground 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
198
+ className
199
+ ),
200
+ ...props
201
+ }
202
+ ) });
203
+ }
204
+
205
+ // src/components/ui/textarea.tsx
206
+ var React4 = require("react");
207
+ var import_jsx_runtime6 = require("react/jsx-runtime");
208
+ function Textarea({ className, ...props }) {
209
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
210
+ "textarea",
211
+ {
212
+ "data-slot": "textarea",
213
+ className: cn(
214
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
215
+ className
216
+ ),
217
+ ...props
218
+ }
219
+ );
220
+ }
221
+
222
+ // src/components/comments/CanvasComments.tsx
223
+ var import_react2 = require("@grapesjs/react");
224
+ var import_react3 = require("react");
225
+ var import_lucide_react2 = require("lucide-react");
226
+ var import_jsx_runtime7 = require("react/jsx-runtime");
227
+ var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
228
+ var keyOfTarget = (t) => t.objectId ? `obj-${t.objectId}` : `sec-${t.sectionId}`;
229
+ function CanvasComments({
230
+ docId,
231
+ composeTarget,
232
+ onComposeConsumed,
233
+ refreshSignal,
234
+ onOpenCountChange
235
+ }) {
236
+ const commentStore = useCommentStore();
237
+ const editor = (0, import_react2.useEditorMaybe)();
238
+ const containerRef = (0, import_react3.useRef)(null);
239
+ const [comments, setComments] = (0, import_react3.useState)([]);
240
+ const [positions, setPositions] = (0, import_react3.useState)({});
241
+ const [activeKey, setActiveKey] = (0, import_react3.useState)(null);
242
+ const [composeInfo, setComposeInfo] = (0, import_react3.useState)(null);
243
+ const [body, setBody] = (0, import_react3.useState)("");
244
+ const reload = (0, import_react3.useCallback)(() => {
245
+ commentStore.list(docId).then(setComments).catch(console.error);
246
+ }, [docId]);
247
+ (0, import_react3.useEffect)(() => {
248
+ reload();
249
+ }, [reload, refreshSignal]);
250
+ const open = (0, import_react3.useMemo)(() => comments.filter((c) => c.status === "open"), [comments]);
251
+ (0, import_react3.useEffect)(() => {
252
+ onOpenCountChange(open.length);
253
+ }, [open.length, onOpenCountChange]);
254
+ const pins = (0, import_react3.useMemo)(() => {
255
+ const map = /* @__PURE__ */ new Map();
256
+ for (const c of open) {
257
+ const k = keyOf(c);
258
+ const arr = map.get(k) ?? [];
259
+ arr.push(c);
260
+ map.set(k, arr);
261
+ }
262
+ return [...map.entries()].map(([key, list]) => ({ key, list }));
263
+ }, [open]);
264
+ const openRef = (0, import_react3.useRef)(open);
265
+ openRef.current = open;
266
+ const activeRef = (0, import_react3.useRef)(activeKey);
267
+ activeRef.current = activeKey;
268
+ const positionsRef = (0, import_react3.useRef)({});
269
+ const recompute = (0, import_react3.useCallback)(() => {
270
+ const ed = editor;
271
+ const container = containerRef.current;
272
+ if (!ed || !container) return;
273
+ const frame = ed.Canvas.getFrameEl?.();
274
+ const cdoc = ed.Canvas.getDocument?.();
275
+ if (!frame || !cdoc) return;
276
+ const cRect = container.getBoundingClientRect();
277
+ const fRect = frame.getBoundingClientRect();
278
+ const zoom = frame.offsetWidth ? fRect.width / frame.offsetWidth : 1;
279
+ const keys = new Set(openRef.current.map(keyOf));
280
+ if (activeRef.current) keys.add(activeRef.current);
281
+ const next = {};
282
+ keys.forEach((k) => {
283
+ const el = cdoc.querySelector(`.${k}`);
284
+ if (!el) return;
285
+ const r = el.getBoundingClientRect();
286
+ next[k] = {
287
+ top: fRect.top - cRect.top + r.top * zoom + 4,
288
+ left: fRect.left - cRect.left + (r.left + r.width) * zoom + 8
289
+ };
290
+ });
291
+ if (JSON.stringify(positionsRef.current) !== JSON.stringify(next)) {
292
+ positionsRef.current = next;
293
+ setPositions(next);
294
+ }
295
+ }, [editor]);
296
+ (0, import_react3.useEffect)(() => {
297
+ const ed = editor;
298
+ if (!ed) return;
299
+ const evts = [
300
+ "canvas:update",
301
+ "update",
302
+ "change:device",
303
+ "load",
304
+ "component:update",
305
+ "component:mount"
306
+ ];
307
+ evts.forEach((e) => ed.on(e, recompute));
308
+ const win = ed.Canvas.getWindow?.();
309
+ win?.addEventListener("scroll", recompute, true);
310
+ window.addEventListener("resize", recompute);
311
+ const id = window.setInterval(recompute, 500);
312
+ return () => {
313
+ evts.forEach((e) => ed.off(e, recompute));
314
+ win?.removeEventListener("scroll", recompute, true);
315
+ window.removeEventListener("resize", recompute);
316
+ window.clearInterval(id);
317
+ };
318
+ }, [editor, recompute]);
319
+ (0, import_react3.useEffect)(() => {
320
+ recompute();
321
+ }, [comments, activeKey, recompute]);
322
+ (0, import_react3.useEffect)(() => {
323
+ if (!composeTarget) return;
324
+ setComposeInfo(composeTarget);
325
+ setActiveKey(keyOfTarget(composeTarget));
326
+ setBody("");
327
+ requestAnimationFrame(recompute);
328
+ }, [composeTarget, recompute]);
329
+ const activeComments = activeKey ? open.filter((c) => keyOf(c) === activeKey) : [];
330
+ const activeTarget = (() => {
331
+ if (!activeKey) return null;
332
+ const c = activeComments[0];
333
+ if (c) return { sectionId: c.sectionId, objectId: c.objectId, objectLabel: c.objectLabel };
334
+ if (composeInfo && keyOfTarget(composeInfo) === activeKey) return composeInfo;
335
+ return null;
336
+ })();
337
+ const activePos = activeKey ? positions[activeKey] : void 0;
338
+ const lastAnchorPos = (0, import_react3.useRef)({ top: 0, left: 0 });
339
+ if (activePos) lastAnchorPos.current = activePos;
340
+ const anchorPos = activePos ?? lastAnchorPos.current;
341
+ const closePopover = (0, import_react3.useCallback)(() => {
342
+ setActiveKey(null);
343
+ setBody("");
344
+ if (composeInfo) {
345
+ setComposeInfo(null);
346
+ onComposeConsumed();
347
+ }
348
+ }, [composeInfo, onComposeConsumed]);
349
+ (0, import_react3.useEffect)(() => {
350
+ if (!activeKey || !editor) return;
351
+ const doc = editor.Canvas.getDocument?.();
352
+ if (!doc) return;
353
+ const onDown = () => closePopover();
354
+ doc.addEventListener("mousedown", onDown, true);
355
+ return () => doc.removeEventListener("mousedown", onDown, true);
356
+ }, [activeKey, editor, closePopover]);
357
+ const submit = async () => {
358
+ if (!activeTarget || !body.trim()) return;
359
+ try {
360
+ await commentStore.add(docId, activeTarget, body.trim());
361
+ setBody("");
362
+ if (composeInfo) {
363
+ setComposeInfo(null);
364
+ onComposeConsumed();
365
+ }
366
+ reload();
367
+ } catch (e) {
368
+ console.error(e);
369
+ }
370
+ };
371
+ const resolve = async (id) => {
372
+ try {
373
+ await commentStore.resolve(id);
374
+ reload();
375
+ } catch (e) {
376
+ console.error(e);
377
+ }
378
+ };
379
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
380
+ pins.map(({ key, list }) => {
381
+ const pos = positions[key];
382
+ if (!pos) return null;
383
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
384
+ "button",
385
+ {
386
+ type: "button",
387
+ onClick: () => {
388
+ setActiveKey((k) => k === key ? null : key);
389
+ setBody("");
390
+ },
391
+ style: { top: pos.top, left: pos.left },
392
+ className: "pointer-events-auto absolute flex h-6 min-w-6 items-center justify-center rounded-full rounded-bl-none bg-brand px-1.5 text-xs font-semibold text-brand-fg shadow-md ring-2 ring-white transition-transform hover:scale-110",
393
+ title: `${list.length} comment(s)`,
394
+ children: list.length
395
+ },
396
+ key
397
+ );
398
+ }),
399
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
400
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
401
+ "div",
402
+ {
403
+ style: {
404
+ position: "absolute",
405
+ top: anchorPos.top,
406
+ left: anchorPos.left,
407
+ width: 1,
408
+ height: 1
409
+ }
410
+ }
411
+ ) }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
413
+ PopoverContent,
414
+ {
415
+ side: "right",
416
+ align: "start",
417
+ className: "pointer-events-auto w-80 p-0 data-[state=closed]:!animate-none data-[state=closed]:!duration-0",
418
+ children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
421
+ "Comment on",
422
+ " ",
423
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
424
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
425
+ "Comment on the whole section",
426
+ " ",
427
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
428
+ ] }) }),
429
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
430
+ "button",
431
+ {
432
+ type: "button",
433
+ onClick: closePopover,
434
+ "aria-label": "Close",
435
+ className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
436
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.X, { className: "size-4" })
437
+ }
438
+ )
439
+ ] }),
440
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
441
+ activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-muted-foreground", children: "No comments yet \u2014 add the first one." }),
442
+ activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
445
+ "button",
446
+ {
447
+ onClick: () => void resolve(c.id),
448
+ className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
449
+ children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.Check, { className: "size-3" }),
451
+ " Resolve"
452
+ ]
453
+ }
454
+ )
455
+ ] }, c.id))
456
+ ] }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "border-t p-3", children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
459
+ Textarea,
460
+ {
461
+ autoFocus: true,
462
+ value: body,
463
+ onChange: (e) => setBody(e.target.value),
464
+ placeholder: "Add a comment\u2026 (\u2318/Ctrl+Enter to send)",
465
+ className: "min-h-14 resize-none text-sm",
466
+ onKeyDown: (e) => {
467
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
468
+ e.preventDefault();
469
+ void submit();
470
+ }
471
+ }
472
+ }
473
+ ),
474
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
475
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.MessageSquarePlus, {}),
476
+ " Add"
477
+ ] }) })
478
+ ] })
479
+ ]
480
+ }
481
+ )
482
+ ] })
483
+ ] });
484
+ }
485
+
486
+ // src/components/editor/EmailEditor.tsx
487
+ var import_sonner = require("sonner");
488
+ var import_lucide_react3 = require("lucide-react");
489
+
490
+ // src/components/ui/switch.tsx
491
+ var React5 = require("react");
492
+ var import_radix_ui3 = require("radix-ui");
493
+ var import_jsx_runtime8 = require("react/jsx-runtime");
494
+ function Switch({
495
+ className,
496
+ size = "default",
497
+ ...props
498
+ }) {
499
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
500
+ import_radix_ui3.Switch.Root,
501
+ {
502
+ "data-slot": "switch",
503
+ "data-size": size,
504
+ className: cn(
505
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
506
+ className
507
+ ),
508
+ ...props,
509
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
510
+ import_radix_ui3.Switch.Thumb,
511
+ {
512
+ "data-slot": "switch-thumb",
513
+ className: "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
514
+ }
515
+ )
516
+ }
517
+ );
518
+ }
519
+
520
+ // src/components/editor/EmailEditor.tsx
521
+ var import_lucide_react4 = require("lucide-react");
522
+ var import_jsx_runtime9 = require("react/jsx-runtime");
523
+ var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
524
+ var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
525
+ var TYPE_LABEL = {
526
+ "mj-text": "Text",
527
+ "mj-button": "Button",
528
+ "mj-image": "Image",
529
+ "mj-column": "Column"
530
+ };
531
+ function tagOf(c) {
532
+ return String(c.get("tagName") || c.get("type") || "");
533
+ }
534
+ var SPARKLES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
535
+ function isSection(c) {
536
+ return tagOf(c) === "mj-section";
537
+ }
538
+ function isCommentable(c) {
539
+ const t = tagOf(c);
540
+ return t === "mj-section" || t in TYPE_LABEL;
541
+ }
542
+ function classMatch(c, re) {
543
+ const cssClass = String(c.getAttributes()["css-class"] ?? "");
544
+ return cssClass.match(re)?.[1] ?? null;
545
+ }
546
+ function sectionIdOf(c) {
547
+ return classMatch(c, SEC_ID_RE);
548
+ }
549
+ function addIdClass(c, prefix) {
550
+ const existing = classMatch(c, prefix === "sec" ? SEC_ID_RE : OBJ_ID_RE);
551
+ if (existing) return existing;
552
+ const cssClass = String(c.getAttributes()["css-class"] ?? "");
553
+ const id = Math.random().toString(36).slice(2, 10);
554
+ c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
555
+ return id;
556
+ }
557
+ function closestSection(c) {
558
+ let cur = c;
559
+ while (cur) {
560
+ if (isSection(cur)) return cur;
561
+ cur = cur.parent() ?? void 0;
562
+ }
563
+ return null;
564
+ }
565
+ var CRUMB_NAME = {
566
+ "mj-section": "Section",
567
+ "mj-column": "Column",
568
+ "mj-group": "Group",
569
+ "mj-text": "Text",
570
+ "mj-button": "Button",
571
+ "mj-image": "Image",
572
+ "mj-divider": "Divider",
573
+ "mj-spacer": "Spacer",
574
+ "mj-hero": "Hero",
575
+ "mj-navbar": "Navbar",
576
+ "mj-social": "Social",
577
+ "mj-social-element": "Social",
578
+ "mj-wrapper": "Wrapper"
579
+ };
580
+ function crumbName(c) {
581
+ const t = tagOf(c);
582
+ return CRUMB_NAME[t] ?? t.replace(/^mj-/, "") ?? "Element";
583
+ }
584
+ function objectLabel(c) {
585
+ const base = TYPE_LABEL[tagOf(c)] ?? tagOf(c);
586
+ const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
587
+ return text ? `${base}: \u201C${text}\u201D` : base;
588
+ }
589
+ var RTE_FONTS = [
590
+ "Arial",
591
+ "Helvetica",
592
+ "Georgia",
593
+ "Times New Roman",
594
+ "Verdana",
595
+ "Tahoma",
596
+ "Trebuchet MS",
597
+ "Courier New"
598
+ ];
599
+ var RTE_SIZES = [11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 40];
600
+ function wrapSelectionStyle(el, style) {
601
+ const doc = el?.ownerDocument;
602
+ const sel = doc?.getSelection?.();
603
+ if (!doc || !sel || sel.rangeCount === 0 || sel.isCollapsed) return;
604
+ const range = sel.getRangeAt(0);
605
+ const span = doc.createElement("span");
606
+ Object.assign(span.style, style);
607
+ try {
608
+ span.appendChild(range.extractContents());
609
+ range.insertNode(span);
610
+ sel.removeAllRanges();
611
+ const nr = doc.createRange();
612
+ nr.selectNodeContents(span);
613
+ sel.addRange(nr);
614
+ } catch {
615
+ }
616
+ }
617
+ function setupRichText(editor) {
618
+ const rte = editor.RichTextEditor;
619
+ if (!rte || rte.__customActions) return;
620
+ rte.__customActions = true;
621
+ const val = (action, sel) => action.btn?.querySelector(sel)?.value ?? "";
622
+ rte.add("fontName", {
623
+ icon: `<select class="gjs-rte-select" title="Font"><option value="">Font</option>${RTE_FONTS.map(
624
+ (f) => `<option value="${f}">${f}</option>`
625
+ ).join("")}</select>`,
626
+ event: "change",
627
+ result: (r, action) => {
628
+ const v = val(action, "select");
629
+ if (v) r.exec("fontName", v);
630
+ }
631
+ });
632
+ rte.add("fontSize", {
633
+ icon: `<select class="gjs-rte-select" title="Size"><option value="">Size</option>${RTE_SIZES.map(
634
+ (s) => `<option value="${s}px">${s}</option>`
635
+ ).join("")}</select>`,
636
+ event: "change",
637
+ result: (r, action) => {
638
+ const v = val(action, "select");
639
+ if (v) wrapSelectionStyle(r.el, { fontSize: v });
640
+ }
641
+ });
642
+ rte.add("forecolor", {
643
+ icon: `<input type="color" class="gjs-rte-color" title="Text color" value="#000000">`,
644
+ event: "change",
645
+ result: (r, action) => r.exec("foreColor", val(action, "input"))
646
+ });
647
+ rte.add("hilitecolor", {
648
+ icon: `<input type="color" class="gjs-rte-color" title="Background color" value="#ffff00">`,
649
+ event: "change",
650
+ result: (r, action) => r.exec("hiliteColor", val(action, "input"))
651
+ });
652
+ }
653
+ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
654
+ const documents = useDocumentStore();
655
+ const loadingRef = (0, import_react5.useRef)(false);
656
+ const saveTimer = (0, import_react5.useRef)(null);
657
+ const saveStatusRef = (0, import_react5.useRef)("idle");
658
+ const stateListeners = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
659
+ const [loading, setLoading] = (0, import_react5.useState)(true);
660
+ const [sidebarView, setSidebarView] = (0, import_react5.useState)("blocks");
661
+ const [composeTarget, setComposeTarget] = (0, import_react5.useState)(null);
662
+ const selectionSource = (0, import_react5.useRef)("other");
663
+ const editorRef = (0, import_react5.useRef)(null);
664
+ const [crumbs, setCrumbs] = (0, import_react5.useState)([]);
665
+ const onEditor = async (editor) => {
666
+ editorRef.current = editor;
667
+ const buildCrumbs = (c) => {
668
+ const chain = [];
669
+ let cur = c;
670
+ while (cur) {
671
+ const t = tagOf(cur);
672
+ if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
673
+ chain.unshift(cur);
674
+ cur = cur.parent() ?? void 0;
675
+ }
676
+ setCrumbs(chain);
677
+ };
678
+ const findByTag = (tag) => {
679
+ let found;
680
+ editor.getWrapper()?.onAll((c) => {
681
+ if (!found && (c.get("tagName") === tag || c.get("type") === tag)) found = c;
682
+ });
683
+ return found;
684
+ };
685
+ const bodyWidth = () => {
686
+ const body = findByTag("mj-body");
687
+ return String(body?.getAttributes?.()?.width ?? "600px");
688
+ };
689
+ const snapshot = () => ({
690
+ device: editor.getDevice(),
691
+ canUndo: editor.UndoManager.hasUndo(),
692
+ canRedo: editor.UndoManager.hasRedo(),
693
+ saveStatus: saveStatusRef.current,
694
+ zoom: Math.round(editor.Canvas.getZoom?.() ?? 100),
695
+ contentWidth: bodyWidth()
696
+ });
697
+ const notifyState = () => {
698
+ const s = snapshot();
699
+ stateListeners.current.forEach((l) => l(s));
700
+ };
701
+ const setSave = (s) => {
702
+ saveStatusRef.current = s;
703
+ notifyState();
704
+ };
705
+ const targetOf = (c) => {
706
+ const section = closestSection(c);
707
+ if (!section || !c) return null;
708
+ const sectionId = addIdClass(section, "sec");
709
+ if (isSection(c)) {
710
+ return { sectionId, objectId: null, objectLabel: null };
711
+ }
712
+ const objectId = addIdClass(c, "obj");
713
+ return { sectionId, objectId, objectLabel: objectLabel(c) };
714
+ };
715
+ const decorate = (c) => {
716
+ if (isSection(c)) addIdClass(c, "sec");
717
+ if (!isCommentable(c)) return;
718
+ const toolbar = [...c.get("toolbar") ?? []];
719
+ if (!toolbar.some((t) => t.command === "open-comments")) {
720
+ toolbar.push({
721
+ command: "open-comments",
722
+ label: SPARKLES_SVG,
723
+ attributes: { title: "Comment on element / section", "data-role": "sparkles" }
724
+ });
725
+ c.set("toolbar", toolbar);
726
+ }
727
+ };
728
+ editor.Commands.add("open-comments", {
729
+ run(ed) {
730
+ const target = targetOf(ed.getSelected() ?? void 0);
731
+ if (target) setComposeTarget(target);
732
+ }
733
+ });
734
+ setupRichText(editor);
735
+ const blockCategory = (label) => {
736
+ const l = label.toLowerCase();
737
+ if (/(column|section)/.test(l)) return "Layout";
738
+ if (/(text|image|button|divider|spacer|social)/.test(l)) return "Basic";
739
+ return "Advanced";
740
+ };
741
+ const bm = editor.BlockManager;
742
+ bm.getAll().forEach(
743
+ (b) => {
744
+ b.set("category", blockCategory(b.get("label") || ""));
745
+ }
746
+ );
747
+ bm.render();
748
+ editor.on("component:add", decorate);
749
+ editor.on("load", () => {
750
+ const body = editor.Canvas.getBody();
751
+ body?.addEventListener(
752
+ "mousedown",
753
+ () => {
754
+ selectionSource.current = "canvas";
755
+ },
756
+ true
757
+ );
758
+ });
759
+ editor.on("component:selected", (c) => {
760
+ if (selectionSource.current === "canvas") setSidebarView("settings");
761
+ selectionSource.current = "other";
762
+ buildCrumbs(c);
763
+ if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
764
+ c.addTrait(
765
+ {
766
+ type: "text",
767
+ name: "src",
768
+ label: "Image (URL)",
769
+ placeholder: "https://\u2026",
770
+ changeProp: true
771
+ },
772
+ { at: 0 }
773
+ );
774
+ }
775
+ });
776
+ editor.on("component:deselected", () => setCrumbs([]));
777
+ const save = async () => {
778
+ if (loadingRef.current) return;
779
+ setSave("saving");
780
+ try {
781
+ await documents.save(docId, {
782
+ mjml: editor.getHtml(),
783
+ projectData: editor.getProjectData()
784
+ });
785
+ setSave("saved");
786
+ } catch (e) {
787
+ setSave("error");
788
+ import_sonner.toast.error("Failed to save changes.");
789
+ throw e;
790
+ }
791
+ };
792
+ editor.on("update", () => {
793
+ notifyState();
794
+ if (loadingRef.current) return;
795
+ if (saveTimer.current) clearTimeout(saveTimer.current);
796
+ saveTimer.current = setTimeout(() => void save().catch(console.error), 1200);
797
+ });
798
+ editor.on("change:device", notifyState);
799
+ const loadMjml = (mjml) => {
800
+ loadingRef.current = true;
801
+ if (saveTimer.current) clearTimeout(saveTimer.current);
802
+ editor.setComponents(mjml || STARTER_MJML);
803
+ editor.getWrapper()?.find("mj-section").forEach(decorate);
804
+ for (const t of Object.keys(TYPE_LABEL)) {
805
+ editor.getWrapper()?.find(t).forEach(decorate);
806
+ }
807
+ setTimeout(() => {
808
+ loadingRef.current = false;
809
+ }, 400);
810
+ };
811
+ const ensureHighlightStyles = () => {
812
+ const cdoc = editor.Canvas.getDocument();
813
+ if (!cdoc || cdoc.getElementById("agent-edit-style")) return;
814
+ const accent = getComputedStyle(document.documentElement).getPropertyValue("--mjml-editor-accent").trim() || "#7c5cfc";
815
+ const style = cdoc.createElement("style");
816
+ style.id = "agent-edit-style";
817
+ style.textContent = `
818
+ @keyframes agentEditPulse {
819
+ 0%, 100% { outline-color: color-mix(in srgb, ${accent} 25%, transparent); }
820
+ 50% { outline-color: color-mix(in srgb, ${accent} 95%, transparent); }
821
+ }
822
+ .agent-editing {
823
+ outline: 3px solid color-mix(in srgb, ${accent} 90%, transparent) !important;
824
+ outline-offset: -3px;
825
+ animation: agentEditPulse 1s ease-in-out infinite;
826
+ transition: outline-color 0.2s;
827
+ }`;
828
+ cdoc.head.appendChild(style);
829
+ };
830
+ const sectionEl = (sectionId) => {
831
+ const comp = editor.getWrapper()?.find("mj-section").find((c) => sectionIdOf(c) === sectionId);
832
+ return comp?.getEl?.() ?? void 0;
833
+ };
834
+ const compiledHtml = () => {
835
+ try {
836
+ const res = editor.runCommand("mjml-code-to-html");
837
+ return res?.html ?? editor.getHtml();
838
+ } catch {
839
+ return editor.getHtml();
840
+ }
841
+ };
842
+ try {
843
+ const doc = await documents.get(docId);
844
+ loadMjml(doc.mjml);
845
+ } catch (e) {
846
+ import_sonner.toast.error("Failed to load the document.");
847
+ console.error(e);
848
+ } finally {
849
+ setLoading(false);
850
+ }
851
+ onReady({
852
+ getMjml: () => editor.getHtml(),
853
+ getCompiledHtml: compiledHtml,
854
+ flushSave: async () => {
855
+ if (saveTimer.current) clearTimeout(saveTimer.current);
856
+ await save();
857
+ },
858
+ reloadFromDb: async () => {
859
+ const fresh = await documents.get(docId);
860
+ if (fresh.mjml !== editor.getHtml()) loadMjml(fresh.mjml);
861
+ },
862
+ highlightSection: (sectionId, on) => {
863
+ ensureHighlightStyles();
864
+ const el = sectionEl(sectionId);
865
+ if (!el) return;
866
+ el.classList.toggle("agent-editing", on);
867
+ },
868
+ getDevices: () => editor.Devices.getDevices().map((d) => ({
869
+ id: String(d.id),
870
+ name: d.getName() || String(d.id)
871
+ })),
872
+ setDevice: (name) => {
873
+ editor.setDevice(name);
874
+ notifyState();
875
+ },
876
+ undo: () => {
877
+ editor.UndoManager.undo();
878
+ notifyState();
879
+ },
880
+ redo: () => {
881
+ editor.UndoManager.redo();
882
+ notifyState();
883
+ },
884
+ setZoom: (z) => {
885
+ editor.Canvas.setZoom?.(Math.max(25, Math.min(200, z)));
886
+ notifyState();
887
+ },
888
+ setContentWidth: (w) => {
889
+ findByTag("mj-body")?.addAttributes({ width: w });
890
+ notifyState();
891
+ },
892
+ onEditorState: (cb) => {
893
+ stateListeners.current.add(cb);
894
+ cb(snapshot());
895
+ return () => stateListeners.current.delete(cb);
896
+ }
897
+ });
898
+ };
899
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
900
+ import_react4.default,
901
+ {
902
+ grapesjs: import_grapesjs.default,
903
+ className: "h-full",
904
+ options: {
905
+ height: "100%",
906
+ storageManager: false,
907
+ fromElement: false,
908
+ panels: { defaults: [] },
909
+ blockManager: { appendTo: "#gjs-blocks" },
910
+ styleManager: { appendTo: "#gjs-styles" },
911
+ traitManager: { appendTo: "#gjs-traits" },
912
+ layerManager: { appendTo: "#gjs-layers" },
913
+ deviceManager: {
914
+ devices: [
915
+ { id: "desktop", name: "Desktop", width: "" },
916
+ { id: "mobile", name: "Mobile", width: "375px", widthMedia: "480px" }
917
+ ]
918
+ },
919
+ plugins: [import_grapesjs_mjml.default]
920
+ },
921
+ onEditor,
922
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex h-full min-h-0 flex-1", children: [
923
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
924
+ LeftSidebar,
925
+ {
926
+ view: sidebarView,
927
+ onViewChange: setSidebarView,
928
+ markLayersSource: () => selectionSource.current = "layers"
929
+ }
930
+ ),
931
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
932
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
933
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react4.Canvas, { className: "h-full" }),
934
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
935
+ CanvasComments,
936
+ {
937
+ docId,
938
+ composeTarget,
939
+ onComposeConsumed: () => setComposeTarget(null),
940
+ refreshSignal: commentsRefresh,
941
+ onOpenCountChange
942
+ }
943
+ ),
944
+ loading && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
945
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Spinner, {}),
946
+ " Loading editor\u2026"
947
+ ] })
948
+ ] }),
949
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
950
+ ] })
951
+ ] })
952
+ }
953
+ );
954
+ }
955
+ function CollapseSection({
956
+ title,
957
+ defaultOpen = true,
958
+ children
959
+ }) {
960
+ const [open, setOpen] = (0, import_react5.useState)(defaultOpen);
961
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "border-b border-panel-border", children: [
962
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
963
+ "button",
964
+ {
965
+ type: "button",
966
+ onClick: () => setOpen((o) => !o),
967
+ className: "flex w-full items-center gap-1.5 px-3 py-2 text-xs font-semibold tracking-wide text-panel-muted-fg uppercase hover:text-panel-fg",
968
+ children: [
969
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
970
+ title
971
+ ]
972
+ }
973
+ ),
974
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn(!open && "hidden"), children })
975
+ ] });
976
+ }
977
+ function LeftSidebar({
978
+ view,
979
+ onViewChange,
980
+ markLayersSource
981
+ }) {
982
+ const editor = (0, import_react4.useEditorMaybe)();
983
+ const [layersExpanded, setLayersExpanded] = (0, import_react5.useState)(false);
984
+ const [blockSearch, setBlockSearch] = (0, import_react5.useState)("");
985
+ const changeView = (v) => {
986
+ if (v === "settings" && editor && !editor.getSelected()) {
987
+ const wrapper = editor.getWrapper();
988
+ const body = wrapper?.find("mj-body")[0] ?? wrapper;
989
+ if (body) editor.select(body);
990
+ }
991
+ onViewChange(v);
992
+ };
993
+ const toggleLayers = (expand) => {
994
+ setLayersExpanded(expand);
995
+ editor?.getWrapper()?.onAll((c) => c.set("open", expand));
996
+ };
997
+ const filterBlocks = (q) => {
998
+ setBlockSearch(q);
999
+ const root = document.getElementById("gjs-blocks");
1000
+ if (!root) return;
1001
+ const term = q.trim().toLowerCase();
1002
+ root.querySelectorAll(".gjs-block").forEach((b) => {
1003
+ const label = (b.querySelector(".gjs-block-label")?.textContent ?? b.textContent ?? "").toLowerCase().trim();
1004
+ b.style.display = !term || label.includes(term) ? "" : "none";
1005
+ });
1006
+ root.querySelectorAll(".gjs-block-category").forEach((cat) => {
1007
+ const anyVisible = [...cat.querySelectorAll(".gjs-block")].some(
1008
+ (b) => b.style.display !== "none"
1009
+ );
1010
+ cat.style.display = anyVisible ? "" : "none";
1011
+ });
1012
+ };
1013
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
1014
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex border-b border-panel-border", children: [
1015
+ ["blocks", "Blocks"],
1016
+ ["settings", "Settings"],
1017
+ ["layers", "Layers"]
1018
+ ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1019
+ "button",
1020
+ {
1021
+ type: "button",
1022
+ onClick: () => changeView(value),
1023
+ className: cn(
1024
+ "relative flex-1 px-3 py-2.5 text-sm transition-colors",
1025
+ "after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:transition-colors",
1026
+ view === value ? "font-medium text-panel-fg after:bg-brand" : "text-panel-muted-fg after:bg-transparent hover:text-panel-fg"
1027
+ ),
1028
+ children: label
1029
+ },
1030
+ value
1031
+ )) }),
1032
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
1033
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
1034
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react4.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
1035
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1036
+ Input,
1037
+ {
1038
+ value: blockSearch,
1039
+ onChange: (e) => filterBlocks(e.target.value),
1040
+ placeholder: "Search blocks\u2026",
1041
+ className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1042
+ }
1043
+ ),
1044
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "\u2318K" })
1045
+ ] }),
1046
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1047
+ ] }),
1048
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1049
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: "Attributes", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-traits" }) }),
1050
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: "Style", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-styles" }) })
1051
+ ] }),
1052
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1053
+ "div",
1054
+ {
1055
+ className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
1056
+ onMouseDownCapture: markLayersSource,
1057
+ children: [
1058
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
1059
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: "Expand all" }),
1060
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1061
+ Switch,
1062
+ {
1063
+ checked: layersExpanded,
1064
+ onCheckedChange: toggleLayers,
1065
+ className: "data-[state=checked]:bg-brand data-[state=unchecked]:bg-panel-border"
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
1070
+ ]
1071
+ }
1072
+ )
1073
+ ] });
1074
+ }
1075
+ function Breadcrumb({
1076
+ crumbs,
1077
+ onSelect
1078
+ }) {
1079
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-muted-foreground/60", children: "Select an element\u2026" }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center gap-0.5", children: [
1080
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: "size-3 opacity-40" }),
1081
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1082
+ "button",
1083
+ {
1084
+ type: "button",
1085
+ onClick: () => onSelect(c),
1086
+ className: cn(
1087
+ "rounded px-1.5 py-0.5 hover:bg-muted hover:text-foreground",
1088
+ i === crumbs.length - 1 && "font-medium text-brand"
1089
+ ),
1090
+ children: crumbName(c)
1091
+ }
1092
+ )
1093
+ ] }, i)) });
1094
+ }
1095
+ // Annotate the CommonJS export names for ESM import in node:
1096
+ 0 && (module.exports = {
1097
+ EmailEditor
1098
+ });
1099
+ //# sourceMappingURL=canvas.cjs.map