@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,1649 @@
1
+ "use client";
2
+ import {
3
+ Badge,
4
+ Button,
5
+ Empty,
6
+ EmptyDescription,
7
+ EmptyHeader,
8
+ EmptyMedia,
9
+ EmptyTitle,
10
+ Input,
11
+ Separator,
12
+ Spinner,
13
+ Textarea,
14
+ Tooltip,
15
+ TooltipContent,
16
+ TooltipTrigger,
17
+ cn
18
+ } from "./chunk-V6RR4VRD.js";
19
+
20
+ // src/stores.tsx
21
+ import { createContext, useContext } from "react";
22
+ import { jsx } from "react/jsx-runtime";
23
+ var StoreContext = createContext(null);
24
+ function EditorStoreProvider({
25
+ stores,
26
+ children
27
+ }) {
28
+ return /* @__PURE__ */ jsx(StoreContext.Provider, { value: stores, children });
29
+ }
30
+ function useStores() {
31
+ const stores = useContext(StoreContext);
32
+ if (!stores) {
33
+ throw new Error(
34
+ "The editor needs an <EditorStoreProvider>. Wrap the editor and pass a DocumentStore and a CommentStore \u2014 see packages/editor/README.md."
35
+ );
36
+ }
37
+ return stores;
38
+ }
39
+ function useDocumentStore() {
40
+ return useStores().documents;
41
+ }
42
+ function useCommentStore() {
43
+ return useStores().comments;
44
+ }
45
+
46
+ // src/starter.ts
47
+ var STARTER_MJML = `<mjml>
48
+ <mj-body background-color="#f4f4f5">
49
+ <mj-section background-color="#ffffff" padding="32px 24px">
50
+ <mj-column>
51
+ <mj-text font-size="22px" font-weight="bold">New email</mj-text>
52
+ <mj-text color="#555555">
53
+ Describe the email you need in the chat on the right \u2014 the agent
54
+ will design the sections, content and images.
55
+ </mj-text>
56
+ </mj-column>
57
+ </mj-section>
58
+ </mj-body>
59
+ </mjml>`;
60
+
61
+ // src/components/editor/EditorHeader.tsx
62
+ import { useEffect, useState } from "react";
63
+ import { toast } from "sonner";
64
+ import {
65
+ Undo2,
66
+ Redo2,
67
+ Code2,
68
+ Copy,
69
+ Eye,
70
+ Download,
71
+ MessageSquare,
72
+ Mail,
73
+ ChevronDown,
74
+ Minus,
75
+ Plus,
76
+ Monitor,
77
+ Smartphone,
78
+ MoreVertical,
79
+ X
80
+ } from "lucide-react";
81
+
82
+ // src/components/ui/toggle-group.tsx
83
+ import * as React2 from "react";
84
+ import "class-variance-authority";
85
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
86
+
87
+ // src/components/ui/toggle.tsx
88
+ import "react";
89
+ import { cva } from "class-variance-authority";
90
+ import { Toggle as TogglePrimitive } from "radix-ui";
91
+ import { jsx as jsx2 } from "react/jsx-runtime";
92
+ var toggleVariants = cva(
93
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
94
+ {
95
+ variants: {
96
+ variant: {
97
+ default: "bg-transparent",
98
+ outline: "border border-input bg-transparent hover:bg-muted"
99
+ },
100
+ size: {
101
+ default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
102
+ sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
103
+ lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2"
104
+ }
105
+ },
106
+ defaultVariants: {
107
+ variant: "default",
108
+ size: "default"
109
+ }
110
+ }
111
+ );
112
+
113
+ // src/components/ui/toggle-group.tsx
114
+ import { jsx as jsx3 } from "react/jsx-runtime";
115
+ var ToggleGroupContext = React2.createContext({
116
+ size: "default",
117
+ variant: "default",
118
+ spacing: 2,
119
+ orientation: "horizontal"
120
+ });
121
+ function ToggleGroup({
122
+ className,
123
+ variant,
124
+ size,
125
+ spacing = 2,
126
+ orientation = "horizontal",
127
+ children,
128
+ ...props
129
+ }) {
130
+ return /* @__PURE__ */ jsx3(
131
+ ToggleGroupPrimitive.Root,
132
+ {
133
+ "data-slot": "toggle-group",
134
+ "data-variant": variant,
135
+ "data-size": size,
136
+ "data-spacing": spacing,
137
+ "data-orientation": orientation,
138
+ style: { "--gap": spacing },
139
+ className: cn(
140
+ "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
141
+ className
142
+ ),
143
+ ...props,
144
+ children: /* @__PURE__ */ jsx3(ToggleGroupContext.Provider, { value: { variant, size, spacing, orientation }, children })
145
+ }
146
+ );
147
+ }
148
+ function ToggleGroupItem({
149
+ className,
150
+ children,
151
+ variant = "default",
152
+ size = "default",
153
+ ...props
154
+ }) {
155
+ const context = React2.useContext(ToggleGroupContext);
156
+ return /* @__PURE__ */ jsx3(
157
+ ToggleGroupPrimitive.Item,
158
+ {
159
+ "data-slot": "toggle-group-item",
160
+ "data-variant": context.variant || variant,
161
+ "data-size": context.size || size,
162
+ "data-spacing": context.spacing,
163
+ className: cn(
164
+ "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
165
+ toggleVariants({
166
+ variant: context.variant || variant,
167
+ size: context.size || size
168
+ }),
169
+ className
170
+ ),
171
+ ...props,
172
+ children
173
+ }
174
+ );
175
+ }
176
+
177
+ // src/components/ui/popover.tsx
178
+ import "react";
179
+ import { Popover as PopoverPrimitive } from "radix-ui";
180
+ import { jsx as jsx4 } from "react/jsx-runtime";
181
+ function Popover({ ...props }) {
182
+ return /* @__PURE__ */ jsx4(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
183
+ }
184
+ function PopoverTrigger({ ...props }) {
185
+ return /* @__PURE__ */ jsx4(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
186
+ }
187
+ function PopoverAnchor({ ...props }) {
188
+ return /* @__PURE__ */ jsx4(PopoverPrimitive.Anchor, { "data-slot": "popover-anchor", ...props });
189
+ }
190
+ function PopoverContent({
191
+ className,
192
+ align = "center",
193
+ sideOffset = 4,
194
+ ...props
195
+ }) {
196
+ return /* @__PURE__ */ jsx4(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx4(
197
+ PopoverPrimitive.Content,
198
+ {
199
+ "data-slot": "popover-content",
200
+ align,
201
+ sideOffset,
202
+ className: cn(
203
+ "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",
204
+ className
205
+ ),
206
+ ...props
207
+ }
208
+ ) });
209
+ }
210
+
211
+ // src/components/ui/tabs.tsx
212
+ import "react";
213
+ import { cva as cva2 } from "class-variance-authority";
214
+ import { Tabs as TabsPrimitive } from "radix-ui";
215
+ import { jsx as jsx5 } from "react/jsx-runtime";
216
+ function Tabs({
217
+ className,
218
+ orientation = "horizontal",
219
+ ...props
220
+ }) {
221
+ return /* @__PURE__ */ jsx5(
222
+ TabsPrimitive.Root,
223
+ {
224
+ "data-slot": "tabs",
225
+ "data-orientation": orientation,
226
+ className: cn("group/tabs flex gap-2 data-horizontal:flex-col", className),
227
+ ...props
228
+ }
229
+ );
230
+ }
231
+ var tabsListVariants = cva2(
232
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
233
+ {
234
+ variants: {
235
+ variant: {
236
+ default: "bg-muted",
237
+ line: "gap-1 bg-transparent"
238
+ }
239
+ },
240
+ defaultVariants: {
241
+ variant: "default"
242
+ }
243
+ }
244
+ );
245
+ function TabsList({
246
+ className,
247
+ variant = "default",
248
+ ...props
249
+ }) {
250
+ return /* @__PURE__ */ jsx5(
251
+ TabsPrimitive.List,
252
+ {
253
+ "data-slot": "tabs-list",
254
+ "data-variant": variant,
255
+ className: cn(tabsListVariants({ variant }), className),
256
+ ...props
257
+ }
258
+ );
259
+ }
260
+ function TabsTrigger({ className, ...props }) {
261
+ return /* @__PURE__ */ jsx5(
262
+ TabsPrimitive.Trigger,
263
+ {
264
+ "data-slot": "tabs-trigger",
265
+ className: cn(
266
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
267
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
268
+ "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
269
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
270
+ className
271
+ ),
272
+ ...props
273
+ }
274
+ );
275
+ }
276
+ function TabsContent({ className, ...props }) {
277
+ return /* @__PURE__ */ jsx5(
278
+ TabsPrimitive.Content,
279
+ {
280
+ "data-slot": "tabs-content",
281
+ className: cn("flex-1 text-sm outline-none", className),
282
+ ...props
283
+ }
284
+ );
285
+ }
286
+
287
+ // src/components/ui/dialog.tsx
288
+ import "react";
289
+ import { Dialog as DialogPrimitive } from "radix-ui";
290
+ import { XIcon } from "lucide-react";
291
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
292
+ function Dialog({ ...props }) {
293
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
294
+ }
295
+ function DialogTrigger({ ...props }) {
296
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Trigger, { "data-slot": "dialog-trigger", ...props });
297
+ }
298
+ function DialogPortal({ ...props }) {
299
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
300
+ }
301
+ function DialogClose({ ...props }) {
302
+ return /* @__PURE__ */ jsx6(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
303
+ }
304
+ function DialogOverlay({
305
+ className,
306
+ ...props
307
+ }) {
308
+ return /* @__PURE__ */ jsx6(
309
+ DialogPrimitive.Overlay,
310
+ {
311
+ "data-slot": "dialog-overlay",
312
+ className: cn(
313
+ "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
314
+ className
315
+ ),
316
+ ...props
317
+ }
318
+ );
319
+ }
320
+ function DialogContent({
321
+ className,
322
+ children,
323
+ showCloseButton = true,
324
+ ...props
325
+ }) {
326
+ return /* @__PURE__ */ jsxs(DialogPortal, { children: [
327
+ /* @__PURE__ */ jsx6(DialogOverlay, {}),
328
+ /* @__PURE__ */ jsxs(
329
+ DialogPrimitive.Content,
330
+ {
331
+ "data-slot": "dialog-content",
332
+ className: cn(
333
+ "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
334
+ className
335
+ ),
336
+ ...props,
337
+ children: [
338
+ children,
339
+ showCloseButton && /* @__PURE__ */ jsx6(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm", children: [
340
+ /* @__PURE__ */ jsx6(XIcon, {}),
341
+ /* @__PURE__ */ jsx6("span", { className: "sr-only", children: "Close" })
342
+ ] }) })
343
+ ]
344
+ }
345
+ )
346
+ ] });
347
+ }
348
+ function DialogHeader({ className, ...props }) {
349
+ return /* @__PURE__ */ jsx6("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props });
350
+ }
351
+ function DialogTitle({ className, ...props }) {
352
+ return /* @__PURE__ */ jsx6(
353
+ DialogPrimitive.Title,
354
+ {
355
+ "data-slot": "dialog-title",
356
+ className: cn("font-heading text-base leading-none font-medium", className),
357
+ ...props
358
+ }
359
+ );
360
+ }
361
+
362
+ // src/components/editor/EditorHeader.tsx
363
+ import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
364
+ var SAVE_LABEL = {
365
+ idle: "",
366
+ saving: "Saving\u2026",
367
+ saved: "Saved",
368
+ error: "Save failed"
369
+ };
370
+ var WIDTHS = ["600px", "640px", "680px", "720px"];
371
+ var DEVICE_ICON = {
372
+ Desktop: /* @__PURE__ */ jsx7(Monitor, { className: "size-3.5" }),
373
+ Mobile: /* @__PURE__ */ jsx7(Smartphone, { className: "size-3.5" })
374
+ };
375
+ function SaveBadge({ status }) {
376
+ if (status === "idle") return null;
377
+ if (status === "saving")
378
+ return /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1 text-xs text-panel-muted-fg", children: [
379
+ /* @__PURE__ */ jsx7(Spinner, { className: "size-3" }),
380
+ " ",
381
+ SAVE_LABEL.saving
382
+ ] });
383
+ if (status === "error") return /* @__PURE__ */ jsx7(Badge, { variant: "destructive", children: SAVE_LABEL.error });
384
+ return /* @__PURE__ */ jsxs2("span", { className: "text-xs text-emerald-400", children: [
385
+ "\u2713 ",
386
+ SAVE_LABEL.saved
387
+ ] });
388
+ }
389
+ function slugify(name) {
390
+ return name.trim().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").toLowerCase() || "email";
391
+ }
392
+ function download(filename, content, mime) {
393
+ const blob = new Blob([content], { type: mime });
394
+ const url = URL.createObjectURL(blob);
395
+ const a = document.createElement("a");
396
+ a.href = url;
397
+ a.download = filename;
398
+ a.click();
399
+ URL.revokeObjectURL(url);
400
+ }
401
+ var darkGhost = "text-panel-muted-fg hover:bg-panel-hover hover:text-panel-fg";
402
+ function EditorHeader({
403
+ docId,
404
+ api,
405
+ openCount,
406
+ homeLink
407
+ }) {
408
+ const documents = useDocumentStore();
409
+ const [name, setName] = useState("");
410
+ const [state, setState] = useState({
411
+ device: "Desktop",
412
+ canUndo: false,
413
+ canRedo: false,
414
+ saveStatus: "idle",
415
+ zoom: 100,
416
+ contentWidth: "600px"
417
+ });
418
+ const [code, setCode] = useState({ mjml: "", html: "" });
419
+ const [preview, setPreview] = useState("");
420
+ useEffect(() => {
421
+ documents.get(docId).then((d) => setName(d.name)).catch(() => {
422
+ });
423
+ }, [docId, documents]);
424
+ useEffect(() => {
425
+ if (!api) return;
426
+ return api.onEditorState(setState);
427
+ }, [api]);
428
+ const commitName = async () => {
429
+ const n = name.trim() || "Untitled";
430
+ setName(n);
431
+ try {
432
+ await documents.save(docId, { name: n });
433
+ } catch {
434
+ toast.error("Failed to rename.");
435
+ }
436
+ };
437
+ const devices = api?.getDevices() ?? [];
438
+ const base = slugify(name);
439
+ const openCode = () => setCode({ mjml: api?.getMjml() ?? "", html: api?.getCompiledHtml() ?? "" });
440
+ return /* @__PURE__ */ jsxs2("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-panel-border bg-panel px-3 text-panel-fg", children: [
441
+ /* @__PURE__ */ jsx7("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-brand to-indigo-400 text-brand-fg shadow-sm", children: /* @__PURE__ */ jsx7(Mail, { className: "size-4" }) }),
442
+ /* @__PURE__ */ jsx7("span", { className: "hidden text-sm font-semibold sm:inline", children: "MJML Editor" }),
443
+ /* @__PURE__ */ jsx7(Separator, { orientation: "vertical", className: "mx-1 !h-5 bg-panel-border" }),
444
+ /* @__PURE__ */ jsx7(
445
+ Input,
446
+ {
447
+ value: name,
448
+ onChange: (e) => setName(e.target.value),
449
+ onBlur: () => void commitName(),
450
+ onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(),
451
+ className: "h-8 w-44 border-transparent bg-transparent text-sm font-medium text-panel-fg hover:border-panel-border focus-visible:border-panel-border",
452
+ "aria-label": "Email name"
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx7(Separator, { orientation: "vertical", className: "mx-1 !h-5 bg-panel-border" }),
456
+ devices.length > 0 && /* @__PURE__ */ jsx7(
457
+ ToggleGroup,
458
+ {
459
+ type: "single",
460
+ size: "sm",
461
+ value: state.device,
462
+ onValueChange: (v) => v && api?.setDevice(v),
463
+ className: "rounded-md border border-panel-border",
464
+ children: devices.map((d) => /* @__PURE__ */ jsxs2(
465
+ ToggleGroupItem,
466
+ {
467
+ value: d.name,
468
+ className: "gap-1.5 px-2.5 text-panel-muted-fg data-[state=on]:bg-panel-elevated data-[state=on]:text-panel-fg",
469
+ children: [
470
+ DEVICE_ICON[d.name],
471
+ d.name
472
+ ]
473
+ },
474
+ d.id
475
+ ))
476
+ }
477
+ ),
478
+ /* @__PURE__ */ jsxs2(Tooltip, { children: [
479
+ /* @__PURE__ */ jsx7(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx7(
480
+ Button,
481
+ {
482
+ variant: "ghost",
483
+ size: "icon",
484
+ className: darkGhost,
485
+ disabled: !state.canUndo,
486
+ onClick: () => api?.undo(),
487
+ children: /* @__PURE__ */ jsx7(Undo2, {})
488
+ }
489
+ ) }),
490
+ /* @__PURE__ */ jsx7(TooltipContent, { children: "Undo" })
491
+ ] }),
492
+ /* @__PURE__ */ jsxs2(Tooltip, { children: [
493
+ /* @__PURE__ */ jsx7(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx7(
494
+ Button,
495
+ {
496
+ variant: "ghost",
497
+ size: "icon",
498
+ className: darkGhost,
499
+ disabled: !state.canRedo,
500
+ onClick: () => api?.redo(),
501
+ children: /* @__PURE__ */ jsx7(Redo2, {})
502
+ }
503
+ ) }),
504
+ /* @__PURE__ */ jsx7(TooltipContent, { children: "Redo" })
505
+ ] }),
506
+ /* @__PURE__ */ jsx7(Separator, { orientation: "vertical", className: "mx-1 !h-5 bg-panel-border max-xl:hidden" }),
507
+ /* @__PURE__ */ jsxs2(Popover, { children: [
508
+ /* @__PURE__ */ jsx7(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
509
+ Button,
510
+ {
511
+ variant: "ghost",
512
+ size: "sm",
513
+ className: cn(darkGhost, "gap-1.5 rounded-md border border-panel-border max-xl:hidden"),
514
+ children: [
515
+ /* @__PURE__ */ jsx7("span", { className: "text-panel-muted-fg", children: "Content width" }),
516
+ state.contentWidth,
517
+ /* @__PURE__ */ jsx7(ChevronDown, { className: "size-3.5" })
518
+ ]
519
+ }
520
+ ) }),
521
+ /* @__PURE__ */ jsx7(PopoverContent, { align: "start", className: "w-40 p-1", children: WIDTHS.map((w) => /* @__PURE__ */ jsx7(
522
+ "button",
523
+ {
524
+ onClick: () => api?.setContentWidth(w),
525
+ className: cn(
526
+ "flex w-full items-center justify-between rounded px-2 py-1.5 text-sm hover:bg-muted",
527
+ state.contentWidth === w && "font-medium text-brand"
528
+ ),
529
+ children: w
530
+ },
531
+ w
532
+ )) })
533
+ ] }),
534
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-0.5 rounded-md border border-panel-border px-0.5 max-xl:hidden", children: [
535
+ /* @__PURE__ */ jsx7(
536
+ Button,
537
+ {
538
+ variant: "ghost",
539
+ size: "icon",
540
+ className: cn(darkGhost, "size-7"),
541
+ onClick: () => api?.setZoom(state.zoom - 10),
542
+ children: /* @__PURE__ */ jsx7(Minus, { className: "size-3.5" })
543
+ }
544
+ ),
545
+ /* @__PURE__ */ jsxs2("span", { className: "w-10 text-center text-xs tabular-nums text-panel-muted-fg", children: [
546
+ state.zoom,
547
+ "%"
548
+ ] }),
549
+ /* @__PURE__ */ jsx7(
550
+ Button,
551
+ {
552
+ variant: "ghost",
553
+ size: "icon",
554
+ className: cn(darkGhost, "size-7"),
555
+ onClick: () => api?.setZoom(state.zoom + 10),
556
+ children: /* @__PURE__ */ jsx7(Plus, { className: "size-3.5" })
557
+ }
558
+ )
559
+ ] }),
560
+ /* @__PURE__ */ jsxs2("div", { className: "ml-auto flex items-center gap-2", children: [
561
+ openCount > 0 && /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1 text-xs text-panel-muted-fg", children: [
562
+ /* @__PURE__ */ jsx7(MessageSquare, { className: "size-3.5" }),
563
+ " ",
564
+ openCount
565
+ ] }),
566
+ /* @__PURE__ */ jsx7(SaveBadge, { status: state.saveStatus }),
567
+ /* @__PURE__ */ jsxs2(Dialog, { children: [
568
+ /* @__PURE__ */ jsx7(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
569
+ Button,
570
+ {
571
+ variant: "ghost",
572
+ size: "sm",
573
+ className: cn(darkGhost, "gap-1.5"),
574
+ onClick: () => setPreview(api?.getCompiledHtml() ?? ""),
575
+ children: [
576
+ /* @__PURE__ */ jsx7(Eye, {}),
577
+ " Preview"
578
+ ]
579
+ }
580
+ ) }),
581
+ /* @__PURE__ */ jsxs2(
582
+ DialogContent,
583
+ {
584
+ showCloseButton: false,
585
+ className: "flex !max-w-none h-screen w-screen flex-col gap-0 rounded-none border-0 bg-zinc-100 p-0 sm:rounded-none",
586
+ children: [
587
+ /* @__PURE__ */ jsxs2("div", { className: "flex h-12 shrink-0 items-center justify-between border-b bg-white px-4", children: [
588
+ /* @__PURE__ */ jsxs2(DialogTitle, { className: "text-sm", children: [
589
+ "Preview \u2014 ",
590
+ name
591
+ ] }),
592
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
593
+ devices.length > 0 && /* @__PURE__ */ jsx7(
594
+ ToggleGroup,
595
+ {
596
+ type: "single",
597
+ size: "sm",
598
+ value: state.device,
599
+ onValueChange: (v) => v && api?.setDevice(v),
600
+ variant: "outline",
601
+ children: devices.map((d) => /* @__PURE__ */ jsx7(ToggleGroupItem, { value: d.name, className: "px-2.5", children: d.name }, d.id))
602
+ }
603
+ ),
604
+ /* @__PURE__ */ jsxs2(
605
+ Button,
606
+ {
607
+ variant: "outline",
608
+ size: "sm",
609
+ onClick: () => download(`${base}.html`, preview, "text/html"),
610
+ children: [
611
+ /* @__PURE__ */ jsx7(Download, {}),
612
+ " Export HTML"
613
+ ]
614
+ }
615
+ ),
616
+ /* @__PURE__ */ jsx7(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx7(Button, { variant: "ghost", size: "icon", "aria-label": "Close preview", children: /* @__PURE__ */ jsx7(X, {}) }) })
617
+ ] })
618
+ ] }),
619
+ /* @__PURE__ */ jsx7("div", { className: "min-h-0 flex-1 overflow-auto p-6", children: /* @__PURE__ */ jsx7(
620
+ "iframe",
621
+ {
622
+ title: "Email preview",
623
+ srcDoc: preview,
624
+ className: cn(
625
+ "mx-auto block h-full min-h-[80vh] rounded-md border bg-white shadow-sm",
626
+ state.device === "Mobile" ? "w-[375px]" : "w-full max-w-[720px]"
627
+ )
628
+ }
629
+ ) })
630
+ ]
631
+ }
632
+ )
633
+ ] }),
634
+ /* @__PURE__ */ jsxs2(Dialog, { children: [
635
+ /* @__PURE__ */ jsx7(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
636
+ Button,
637
+ {
638
+ variant: "ghost",
639
+ size: "sm",
640
+ className: cn(darkGhost, "gap-1.5"),
641
+ onClick: openCode,
642
+ children: [
643
+ /* @__PURE__ */ jsx7(Code2, {}),
644
+ " Code"
645
+ ]
646
+ }
647
+ ) }),
648
+ /* @__PURE__ */ jsxs2(DialogContent, { className: "flex !max-w-4xl flex-col", children: [
649
+ /* @__PURE__ */ jsx7(DialogHeader, { children: /* @__PURE__ */ jsx7(DialogTitle, { children: "Source code" }) }),
650
+ /* @__PURE__ */ jsxs2(Tabs, { defaultValue: "mjml", className: "min-h-0 flex-1", children: [
651
+ /* @__PURE__ */ jsxs2(TabsList, { children: [
652
+ /* @__PURE__ */ jsx7(TabsTrigger, { value: "mjml", children: "MJML" }),
653
+ /* @__PURE__ */ jsx7(TabsTrigger, { value: "html", children: "HTML" })
654
+ ] }),
655
+ ["mjml", "html"].map((kind) => /* @__PURE__ */ jsxs2(TabsContent, { value: kind, className: "mt-2", children: [
656
+ /* @__PURE__ */ jsx7("pre", { className: "max-h-[62vh] overflow-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed break-all whitespace-pre-wrap", children: code[kind] }),
657
+ /* @__PURE__ */ jsxs2("div", { className: "mt-2 flex justify-end gap-2", children: [
658
+ /* @__PURE__ */ jsxs2(
659
+ Button,
660
+ {
661
+ variant: "outline",
662
+ size: "sm",
663
+ onClick: () => download(
664
+ `${base}.${kind}`,
665
+ code[kind],
666
+ kind === "html" ? "text/html" : "text/plain"
667
+ ),
668
+ children: [
669
+ /* @__PURE__ */ jsx7(Download, {}),
670
+ " Download .",
671
+ kind
672
+ ]
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsxs2(
676
+ Button,
677
+ {
678
+ variant: "secondary",
679
+ size: "sm",
680
+ onClick: () => {
681
+ void navigator.clipboard?.writeText(code[kind]);
682
+ toast.success(`${kind.toUpperCase()} copied.`);
683
+ },
684
+ children: [
685
+ /* @__PURE__ */ jsx7(Copy, {}),
686
+ " Copy"
687
+ ]
688
+ }
689
+ )
690
+ ] })
691
+ ] }, kind))
692
+ ] })
693
+ ] })
694
+ ] }),
695
+ /* @__PURE__ */ jsxs2(Popover, { children: [
696
+ /* @__PURE__ */ jsx7(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(Button, { size: "sm", className: "gap-1.5 bg-brand text-brand-fg hover:bg-brand/90", children: [
697
+ /* @__PURE__ */ jsx7(Download, {}),
698
+ " Export",
699
+ /* @__PURE__ */ jsx7(ChevronDown, { className: "size-3.5" })
700
+ ] }) }),
701
+ /* @__PURE__ */ jsxs2(PopoverContent, { align: "end", className: "w-52 p-1", children: [
702
+ /* @__PURE__ */ jsxs2(
703
+ "button",
704
+ {
705
+ onClick: () => download(`${base}.html`, api?.getCompiledHtml() ?? "", "text/html"),
706
+ className: "flex w-full flex-col rounded px-2 py-1.5 text-left text-sm hover:bg-muted",
707
+ children: [
708
+ /* @__PURE__ */ jsx7("span", { className: "font-medium", children: "Export HTML" }),
709
+ /* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground", children: [
710
+ base,
711
+ ".html"
712
+ ] })
713
+ ]
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsxs2(
717
+ "button",
718
+ {
719
+ onClick: () => download(`${base}.mjml`, api?.getMjml() ?? "", "text/plain"),
720
+ className: "flex w-full flex-col rounded px-2 py-1.5 text-left text-sm hover:bg-muted",
721
+ children: [
722
+ /* @__PURE__ */ jsx7("span", { className: "font-medium", children: "Export MJML" }),
723
+ /* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground", children: [
724
+ base,
725
+ ".mjml"
726
+ ] })
727
+ ]
728
+ }
729
+ )
730
+ ] })
731
+ ] }),
732
+ /* @__PURE__ */ jsxs2(Popover, { children: [
733
+ /* @__PURE__ */ jsx7(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx7(Button, { variant: "ghost", size: "icon", className: darkGhost, children: /* @__PURE__ */ jsx7(MoreVertical, {}) }) }),
734
+ /* @__PURE__ */ jsx7(PopoverContent, { align: "end", className: "w-44 p-1", children: homeLink })
735
+ ] })
736
+ ] })
737
+ ] });
738
+ }
739
+
740
+ // src/components/chat/ChatPanel.tsx
741
+ import { useChat } from "@ai-sdk/react";
742
+ import { DefaultChatTransport } from "ai";
743
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
744
+ import { toast as toast2 } from "sonner";
745
+ import {
746
+ Check,
747
+ Loader2,
748
+ TriangleAlert,
749
+ Send,
750
+ Square,
751
+ Sparkles,
752
+ MessageSquare as MessageSquare2,
753
+ ChevronRight
754
+ } from "lucide-react";
755
+
756
+ // src/components/ui/message.tsx
757
+ import "react";
758
+ import { jsx as jsx8 } from "react/jsx-runtime";
759
+ function Message({
760
+ className,
761
+ align = "start",
762
+ ...props
763
+ }) {
764
+ return /* @__PURE__ */ jsx8(
765
+ "div",
766
+ {
767
+ "data-slot": "message",
768
+ "data-align": align,
769
+ className: cn(
770
+ "group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse",
771
+ className
772
+ ),
773
+ ...props
774
+ }
775
+ );
776
+ }
777
+ function MessageContent({ className, ...props }) {
778
+ return /* @__PURE__ */ jsx8(
779
+ "div",
780
+ {
781
+ "data-slot": "message-content",
782
+ className: cn(
783
+ "flex w-full min-w-0 flex-col gap-2.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
784
+ className
785
+ ),
786
+ ...props
787
+ }
788
+ );
789
+ }
790
+
791
+ // src/components/ui/bubble.tsx
792
+ import "react";
793
+ import { cva as cva3 } from "class-variance-authority";
794
+ import { Slot } from "radix-ui";
795
+ import { jsx as jsx9 } from "react/jsx-runtime";
796
+ var bubbleVariants = cva3(
797
+ "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
798
+ {
799
+ variants: {
800
+ variant: {
801
+ default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
802
+ secondary: "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
803
+ muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
804
+ tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
805
+ outline: "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
806
+ ghost: "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
807
+ destructive: "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30"
808
+ }
809
+ },
810
+ defaultVariants: {
811
+ variant: "default"
812
+ }
813
+ }
814
+ );
815
+ function Bubble({
816
+ variant = "default",
817
+ align = "start",
818
+ className,
819
+ ...props
820
+ }) {
821
+ return /* @__PURE__ */ jsx9(
822
+ "div",
823
+ {
824
+ "data-slot": "bubble",
825
+ "data-variant": variant,
826
+ "data-align": align,
827
+ className: cn(bubbleVariants({ variant }), className),
828
+ ...props
829
+ }
830
+ );
831
+ }
832
+ function BubbleContent({
833
+ asChild = false,
834
+ className,
835
+ ...props
836
+ }) {
837
+ const Comp = asChild ? Slot.Root : "div";
838
+ return /* @__PURE__ */ jsx9(
839
+ Comp,
840
+ {
841
+ "data-slot": "bubble-content",
842
+ className: cn(
843
+ "w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
844
+ className
845
+ ),
846
+ ...props
847
+ }
848
+ );
849
+ }
850
+ var bubbleReactionsVariants = cva3(
851
+ "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",
852
+ {
853
+ variants: {
854
+ side: {
855
+ top: "top-0 -translate-y-3/4",
856
+ bottom: "bottom-0 translate-y-3/4"
857
+ },
858
+ align: {
859
+ start: "left-3",
860
+ end: "right-3"
861
+ }
862
+ },
863
+ defaultVariants: {
864
+ side: "bottom",
865
+ align: "end"
866
+ }
867
+ }
868
+ );
869
+
870
+ // src/components/chat/Markdown.tsx
871
+ import { memo } from "react";
872
+ import ReactMarkdown from "react-markdown";
873
+ import remarkGfm from "remark-gfm";
874
+ import { jsx as jsx10 } from "react/jsx-runtime";
875
+ var components = {
876
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
877
+ a: ({ node, ...props }) => /* @__PURE__ */ jsx10("a", { target: "_blank", rel: "noopener noreferrer", ...props })
878
+ };
879
+ function MarkdownImpl({ children }) {
880
+ return /* @__PURE__ */ jsx10(
881
+ "div",
882
+ {
883
+ className: cn(
884
+ "text-sm leading-relaxed break-words",
885
+ "[&>:first-child]:mt-0 [&>:last-child]:mb-0",
886
+ "[&_p]:my-1.5",
887
+ "[&_ul]:my-1.5 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-1.5 [&_ol]:list-decimal [&_ol]:pl-5",
888
+ "[&_li]:my-0.5 [&_li>ul]:my-0.5 [&_li>ol]:my-0.5",
889
+ "[&_strong]:font-semibold [&_em]:italic",
890
+ "[&_a]:underline [&_a]:underline-offset-2",
891
+ "[&_h1]:mt-2 [&_h1]:mb-1 [&_h1]:text-base [&_h1]:font-semibold",
892
+ "[&_h2]:mt-2 [&_h2]:mb-1 [&_h2]:text-sm [&_h2]:font-semibold",
893
+ "[&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold",
894
+ "[&_code]:rounded [&_code]:bg-black/5 [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em]",
895
+ "[&_pre]:my-1.5 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-black/5 [&_pre]:p-2 [&_pre]:text-xs",
896
+ "[&_pre_code]:bg-transparent [&_pre_code]:p-0",
897
+ "[&_blockquote]:my-1.5 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
898
+ "[&_hr]:my-2 [&_hr]:border-border",
899
+ "[&_table]:my-1.5 [&_table]:w-full [&_table]:border-collapse [&_table]:text-xs",
900
+ "[&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:font-semibold",
901
+ "[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1"
902
+ ),
903
+ children: /* @__PURE__ */ jsx10(ReactMarkdown, { remarkPlugins: [remarkGfm], components, children })
904
+ }
905
+ );
906
+ }
907
+ var Markdown = memo(MarkdownImpl);
908
+
909
+ // src/components/ui/marker.tsx
910
+ import "react";
911
+ import { cva as cva4 } from "class-variance-authority";
912
+ import { Slot as Slot2 } from "radix-ui";
913
+ import { jsx as jsx11 } from "react/jsx-runtime";
914
+ var markerVariants = cva4(
915
+ "group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
916
+ {
917
+ variants: {
918
+ variant: {
919
+ default: "",
920
+ separator: "before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
921
+ border: "border-b border-border pb-2"
922
+ }
923
+ }
924
+ }
925
+ );
926
+ function Marker({
927
+ className,
928
+ variant = "default",
929
+ asChild = false,
930
+ ...props
931
+ }) {
932
+ const Comp = asChild ? Slot2.Root : "div";
933
+ return /* @__PURE__ */ jsx11(
934
+ Comp,
935
+ {
936
+ "data-slot": "marker",
937
+ "data-variant": variant,
938
+ className: cn(markerVariants({ variant, className })),
939
+ ...props
940
+ }
941
+ );
942
+ }
943
+ function MarkerIcon({ className, ...props }) {
944
+ return /* @__PURE__ */ jsx11(
945
+ "span",
946
+ {
947
+ "data-slot": "marker-icon",
948
+ "aria-hidden": "true",
949
+ className: cn("size-4 shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
950
+ ...props
951
+ }
952
+ );
953
+ }
954
+ function MarkerContent({ className, ...props }) {
955
+ return /* @__PURE__ */ jsx11(
956
+ "span",
957
+ {
958
+ "data-slot": "marker-content",
959
+ className: cn(
960
+ "min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
961
+ className
962
+ ),
963
+ ...props
964
+ }
965
+ );
966
+ }
967
+
968
+ // src/components/ui/collapsible.tsx
969
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui";
970
+ import { jsx as jsx12 } from "react/jsx-runtime";
971
+ function Collapsible({ ...props }) {
972
+ return /* @__PURE__ */ jsx12(CollapsiblePrimitive.Root, { "data-slot": "collapsible", ...props });
973
+ }
974
+ function CollapsibleTrigger({
975
+ ...props
976
+ }) {
977
+ return /* @__PURE__ */ jsx12(CollapsiblePrimitive.CollapsibleTrigger, { "data-slot": "collapsible-trigger", ...props });
978
+ }
979
+ function CollapsibleContent({
980
+ ...props
981
+ }) {
982
+ return /* @__PURE__ */ jsx12(CollapsiblePrimitive.CollapsibleContent, { "data-slot": "collapsible-content", ...props });
983
+ }
984
+
985
+ // src/components/ui/message-scroller.tsx
986
+ import "react";
987
+ import {
988
+ MessageScroller as MessageScrollerPrimitive,
989
+ useMessageScroller,
990
+ useMessageScrollerScrollable,
991
+ useMessageScrollerVisibility
992
+ } from "@shadcn/react/message-scroller";
993
+ import { ArrowDownIcon } from "lucide-react";
994
+ import { Fragment, jsx as jsx13, jsxs as jsxs3 } from "react/jsx-runtime";
995
+ function MessageScrollerProvider(props) {
996
+ return /* @__PURE__ */ jsx13(MessageScrollerPrimitive.Provider, { ...props });
997
+ }
998
+ function MessageScroller({
999
+ className,
1000
+ ...props
1001
+ }) {
1002
+ return /* @__PURE__ */ jsx13(
1003
+ MessageScrollerPrimitive.Root,
1004
+ {
1005
+ "data-slot": "message-scroller",
1006
+ className: cn(
1007
+ "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
1008
+ className
1009
+ ),
1010
+ ...props
1011
+ }
1012
+ );
1013
+ }
1014
+ function MessageScrollerViewport({
1015
+ className,
1016
+ ...props
1017
+ }) {
1018
+ return /* @__PURE__ */ jsx13(
1019
+ MessageScrollerPrimitive.Viewport,
1020
+ {
1021
+ "data-slot": "message-scroller-viewport",
1022
+ className: cn(
1023
+ "size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent",
1024
+ className
1025
+ ),
1026
+ ...props
1027
+ }
1028
+ );
1029
+ }
1030
+ function MessageScrollerContent({
1031
+ className,
1032
+ ...props
1033
+ }) {
1034
+ return /* @__PURE__ */ jsx13(
1035
+ MessageScrollerPrimitive.Content,
1036
+ {
1037
+ "data-slot": "message-scroller-content",
1038
+ className: cn("flex h-max min-h-full flex-col gap-6", className),
1039
+ ...props
1040
+ }
1041
+ );
1042
+ }
1043
+ function MessageScrollerItem({
1044
+ className,
1045
+ scrollAnchor = false,
1046
+ ...props
1047
+ }) {
1048
+ return /* @__PURE__ */ jsx13(
1049
+ MessageScrollerPrimitive.Item,
1050
+ {
1051
+ "data-slot": "message-scroller-item",
1052
+ scrollAnchor,
1053
+ className: cn(
1054
+ "min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
1055
+ className
1056
+ ),
1057
+ ...props
1058
+ }
1059
+ );
1060
+ }
1061
+ function MessageScrollerButton({
1062
+ direction = "end",
1063
+ className,
1064
+ children,
1065
+ render,
1066
+ variant = "secondary",
1067
+ size = "icon-sm",
1068
+ ...props
1069
+ }) {
1070
+ return /* @__PURE__ */ jsx13(
1071
+ MessageScrollerPrimitive.Button,
1072
+ {
1073
+ "data-slot": "message-scroller-button",
1074
+ "data-direction": direction,
1075
+ "data-variant": variant,
1076
+ "data-size": size,
1077
+ direction,
1078
+ className: cn(
1079
+ "absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
1080
+ className
1081
+ ),
1082
+ render: render ?? /* @__PURE__ */ jsx13(Button, { variant, size }),
1083
+ ...props,
1084
+ children: children ?? /* @__PURE__ */ jsxs3(Fragment, { children: [
1085
+ /* @__PURE__ */ jsx13(ArrowDownIcon, {}),
1086
+ /* @__PURE__ */ jsx13("span", { className: "sr-only", children: direction === "end" ? "Scroll to end" : "Scroll to start" })
1087
+ ] })
1088
+ }
1089
+ );
1090
+ }
1091
+
1092
+ // src/components/chat/ChatPanel.tsx
1093
+ import { jsx as jsx14, jsxs as jsxs4 } from "react/jsx-runtime";
1094
+ var TOOL_LABELS = {
1095
+ get_document: "Reading document",
1096
+ get_section: "Reading section",
1097
+ set_document: "Saving whole email",
1098
+ set_section: "Replacing section",
1099
+ insert_section: "Adding section",
1100
+ remove_section: "Removing section",
1101
+ generate_image: "Generating image",
1102
+ list_open_comments: "Reading comments",
1103
+ resolve_comment: "Resolving comment"
1104
+ };
1105
+ var APPLY_COMMENTS_PROMPT = "Apply changes based on all open section comments. After each successful change mark the comment as resolved and summarize what you changed.";
1106
+ var MUTATING_TOOLS = /* @__PURE__ */ new Set([
1107
+ "set_document",
1108
+ "set_section",
1109
+ "insert_section",
1110
+ "remove_section",
1111
+ "resolve_comment"
1112
+ ]);
1113
+ var SECTION_ARG = {
1114
+ set_section: "section_id",
1115
+ remove_section: "section_id",
1116
+ insert_section: "after_section_id"
1117
+ };
1118
+ function ToolMarker({ tool }) {
1119
+ const name = tool.type.slice(5);
1120
+ const done = tool.state === "output-available";
1121
+ const failed = tool.state === "output-error";
1122
+ return /* @__PURE__ */ jsxs4(Marker, { className: cn(failed ? "text-destructive" : "text-panel-fg"), children: [
1123
+ /* @__PURE__ */ jsx14(MarkerIcon, { children: failed ? /* @__PURE__ */ jsx14(TriangleAlert, {}) : done ? /* @__PURE__ */ jsx14(Check, { className: "text-emerald-400" }) : /* @__PURE__ */ jsx14(Loader2, { className: "animate-spin" }) }),
1124
+ /* @__PURE__ */ jsxs4(MarkerContent, { children: [
1125
+ TOOL_LABELS[name] ?? name,
1126
+ failed && tool.errorText ? `: ${tool.errorText}` : ""
1127
+ ] })
1128
+ ] });
1129
+ }
1130
+ function BusyMarker() {
1131
+ return /* @__PURE__ */ jsxs4(Marker, { className: "text-panel-fg", children: [
1132
+ /* @__PURE__ */ jsx14(MarkerIcon, { children: /* @__PURE__ */ jsx14(Loader2, { className: "animate-spin" }) }),
1133
+ /* @__PURE__ */ jsx14(MarkerContent, { children: "Agent is working\u2026" })
1134
+ ] });
1135
+ }
1136
+ function ToolActivity({ tools }) {
1137
+ const running = tools.filter((t) => t.state !== "output-available" && t.state !== "output-error");
1138
+ const finished = tools.filter(
1139
+ (t) => t.state === "output-available" || t.state === "output-error"
1140
+ );
1141
+ const failedCount = finished.filter((t) => t.state === "output-error").length;
1142
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-1 rounded-lg border border-panel-border bg-panel-elevated/60 p-2", children: [
1143
+ finished.length > 0 && /* @__PURE__ */ jsxs4(Collapsible, { defaultOpen: true, children: [
1144
+ /* @__PURE__ */ jsxs4(CollapsibleTrigger, { className: "group/col flex w-full items-center gap-1.5 text-left text-xs font-medium text-panel-fg hover:text-panel-fg", children: [
1145
+ /* @__PURE__ */ jsx14(ChevronRight, { className: "size-3.5 shrink-0 transition-transform group-data-[state=open]/col:rotate-90" }),
1146
+ /* @__PURE__ */ jsx14("span", { children: failedCount > 0 ? `Completed steps (${finished.length}, ${failedCount} failed)` : `Completed steps (${finished.length})` })
1147
+ ] }),
1148
+ /* @__PURE__ */ jsx14(CollapsibleContent, { className: "mt-1 space-y-0.5 pl-1", children: finished.map((t, i) => /* @__PURE__ */ jsx14(ToolMarker, { tool: t }, `f${i}`)) })
1149
+ ] }),
1150
+ running.map((t, i) => /* @__PURE__ */ jsx14(ToolMarker, { tool: t }, `r${i}`))
1151
+ ] });
1152
+ }
1153
+ function ChatPanel({
1154
+ docId,
1155
+ onBeforeSend,
1156
+ onAgentFinish,
1157
+ onLiveUpdate,
1158
+ onSectionEditStart,
1159
+ onSectionEditEnd
1160
+ }) {
1161
+ const [input, setInput] = useState2("");
1162
+ const taRef = useRef(null);
1163
+ const times = useRef(/* @__PURE__ */ new Map());
1164
+ const timeFor = (id) => {
1165
+ if (!times.current.has(id)) {
1166
+ times.current.set(
1167
+ id,
1168
+ (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
1169
+ );
1170
+ }
1171
+ return times.current.get(id);
1172
+ };
1173
+ const { messages, sendMessage, status, stop } = useChat({
1174
+ transport: new DefaultChatTransport({
1175
+ // Same-origin: the agent is a route handler in this app, not a separate service.
1176
+ api: "/api/chat",
1177
+ body: { docId }
1178
+ }),
1179
+ onFinish: onAgentFinish,
1180
+ onError: (e) => {
1181
+ console.error("chat error:", e);
1182
+ toast2.error("Agent chat error.");
1183
+ }
1184
+ });
1185
+ const firedTools = useRef(/* @__PURE__ */ new Set());
1186
+ const highlightStarted = useRef(/* @__PURE__ */ new Map());
1187
+ const highlightEnded = useRef(/* @__PURE__ */ new Set());
1188
+ useEffect2(() => {
1189
+ for (const message of messages) {
1190
+ if (message.role !== "assistant") continue;
1191
+ message.parts.forEach((part, i) => {
1192
+ if (!part.type.startsWith("tool-")) return;
1193
+ const tool = part;
1194
+ const name = tool.type.slice(5);
1195
+ const key = `${message.id}:${i}`;
1196
+ const done = tool.state === "output-available" || tool.state === "output-error";
1197
+ const argName = SECTION_ARG[name];
1198
+ if (argName && !done && !highlightStarted.current.has(key)) {
1199
+ const sid = tool.input?.[argName];
1200
+ if (typeof sid === "string" && sid) {
1201
+ highlightStarted.current.set(key, sid);
1202
+ onSectionEditStart(sid);
1203
+ }
1204
+ }
1205
+ if (!done) return;
1206
+ const startedSid = highlightStarted.current.get(key);
1207
+ if (startedSid && !highlightEnded.current.has(key)) {
1208
+ highlightEnded.current.add(key);
1209
+ onSectionEditEnd(startedSid);
1210
+ }
1211
+ if (MUTATING_TOOLS.has(name) && !firedTools.current.has(key)) {
1212
+ firedTools.current.add(key);
1213
+ onLiveUpdate();
1214
+ }
1215
+ });
1216
+ }
1217
+ }, [messages, onLiveUpdate, onSectionEditStart, onSectionEditEnd]);
1218
+ const busy = status === "submitted" || status === "streaming";
1219
+ const lastMsg = messages[messages.length - 1];
1220
+ const toolRunning = lastMsg?.role === "assistant" && lastMsg.parts.some((p) => {
1221
+ if (!p.type.startsWith("tool-")) return false;
1222
+ const s = p.state;
1223
+ return s !== "output-available" && s !== "output-error";
1224
+ });
1225
+ const grow = () => {
1226
+ const el = taRef.current;
1227
+ if (!el) return;
1228
+ el.style.height = "auto";
1229
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
1230
+ };
1231
+ const send = async (text) => {
1232
+ const trimmed = text.trim();
1233
+ if (!trimmed || busy) return;
1234
+ await onBeforeSend().catch(console.error);
1235
+ void sendMessage({ text: trimmed });
1236
+ setInput("");
1237
+ if (taRef.current) taRef.current.style.height = "auto";
1238
+ };
1239
+ return /* @__PURE__ */ jsxs4("div", { className: "flex h-full flex-col", children: [
1240
+ /* @__PURE__ */ jsx14(MessageScrollerProvider, { autoScroll: true, children: /* @__PURE__ */ jsxs4(MessageScroller, { className: "flex-1", children: [
1241
+ /* @__PURE__ */ jsx14(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs4(MessageScrollerContent, { className: "p-3", children: [
1242
+ messages.length === 0 ? /* @__PURE__ */ jsx14(Empty, { className: "h-full", children: /* @__PURE__ */ jsxs4(EmptyHeader, { children: [
1243
+ /* @__PURE__ */ jsx14(EmptyMedia, { variant: "icon", children: /* @__PURE__ */ jsx14(MessageSquare2, {}) }),
1244
+ /* @__PURE__ */ jsx14(EmptyTitle, { children: "Start a conversation with the agent" }),
1245
+ /* @__PURE__ */ jsx14(EmptyDescription, { children: "Describe the email (goal, tone, content) and paste your data \u2014 the agent will design the sections and images." })
1246
+ ] }) }) : messages.map((message, mi) => {
1247
+ const isUser = message.role === "user";
1248
+ const isLast = mi === messages.length - 1;
1249
+ const toolParts = message.parts.filter(
1250
+ (p) => p.type.startsWith("tool-")
1251
+ );
1252
+ const textParts = message.parts.filter(
1253
+ (p) => p.type === "text" && p.text.trim() !== ""
1254
+ );
1255
+ return /* @__PURE__ */ jsx14(MessageScrollerItem, { messageId: message.id, children: /* @__PURE__ */ jsxs4("div", { className: cn("flex w-full gap-2", isUser && "flex-row-reverse"), children: [
1256
+ !isUser && /* @__PURE__ */ jsx14("span", { className: "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-brand/15 text-brand", children: /* @__PURE__ */ jsx14(Sparkles, { className: "size-3.5" }) }),
1257
+ /* @__PURE__ */ jsxs4(
1258
+ "div",
1259
+ {
1260
+ className: cn(
1261
+ "flex min-w-0 flex-1 flex-col gap-1",
1262
+ isUser ? "items-end" : "items-start"
1263
+ ),
1264
+ children: [
1265
+ /* @__PURE__ */ jsx14(Message, { align: isUser ? "end" : "start", children: /* @__PURE__ */ jsxs4(MessageContent, { children: [
1266
+ toolParts.length > 0 && /* @__PURE__ */ jsx14(ToolActivity, { tools: toolParts }),
1267
+ textParts.map((part, i) => /* @__PURE__ */ jsx14(
1268
+ Bubble,
1269
+ {
1270
+ variant: isUser ? "default" : "muted",
1271
+ align: isUser ? "end" : "start",
1272
+ className: cn(
1273
+ isUser ? "[&_[data-slot=bubble-content]]:bg-brand [&_[data-slot=bubble-content]]:text-brand-fg" : "[&_[data-slot=bubble-content]]:border [&_[data-slot=bubble-content]]:border-panel-border [&_[data-slot=bubble-content]]:bg-panel-elevated [&_[data-slot=bubble-content]]:text-panel-fg [&_code]:!bg-white/12 [&_pre]:!bg-white/10 [&_a]:text-brand"
1274
+ ),
1275
+ children: /* @__PURE__ */ jsx14(
1276
+ BubbleContent,
1277
+ {
1278
+ className: isUser ? "whitespace-pre-wrap" : void 0,
1279
+ children: isUser && part.type === "text" ? part.text : part.type === "text" ? /* @__PURE__ */ jsx14(Markdown, { children: part.text }) : null
1280
+ }
1281
+ )
1282
+ },
1283
+ i
1284
+ )),
1285
+ isLast && !isUser && busy && !toolRunning && textParts.length === 0 && /* @__PURE__ */ jsx14(BusyMarker, {})
1286
+ ] }) }),
1287
+ (textParts.length > 0 || toolParts.length > 0) && /* @__PURE__ */ jsx14("span", { className: "px-1 text-[10px] text-panel-muted-fg", children: timeFor(message.id) })
1288
+ ]
1289
+ }
1290
+ )
1291
+ ] }) }, message.id);
1292
+ }),
1293
+ busy && !toolRunning && lastMsg?.role !== "assistant" && /* @__PURE__ */ jsx14(BusyMarker, {})
1294
+ ] }) }),
1295
+ /* @__PURE__ */ jsx14(MessageScrollerButton, { direction: "end" })
1296
+ ] }) }),
1297
+ /* @__PURE__ */ jsxs4("div", { className: "border-t border-panel-border p-3", children: [
1298
+ /* @__PURE__ */ jsxs4(
1299
+ Button,
1300
+ {
1301
+ className: "mb-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1302
+ disabled: busy,
1303
+ onClick: () => void send(APPLY_COMMENTS_PROMPT),
1304
+ children: [
1305
+ /* @__PURE__ */ jsx14(Sparkles, {}),
1306
+ " Apply changes from comments"
1307
+ ]
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ jsxs4(
1311
+ "form",
1312
+ {
1313
+ onSubmit: (e) => {
1314
+ e.preventDefault();
1315
+ void send(input);
1316
+ },
1317
+ className: "rounded-xl border border-panel-border bg-panel-elevated transition-colors focus-within:border-brand/60",
1318
+ children: [
1319
+ /* @__PURE__ */ jsx14(
1320
+ Textarea,
1321
+ {
1322
+ ref: taRef,
1323
+ rows: 2,
1324
+ className: "max-h-40 min-h-14 resize-none border-0 bg-transparent px-3 pt-2.5 text-panel-fg shadow-none placeholder:text-panel-muted-fg focus-visible:ring-0",
1325
+ placeholder: "Describe what you want to change\u2026 (Enter = send, Shift+Enter = new line)",
1326
+ value: input,
1327
+ onChange: (e) => {
1328
+ setInput(e.target.value);
1329
+ grow();
1330
+ },
1331
+ onKeyDown: (e) => {
1332
+ if (e.key === "Enter" && !e.shiftKey) {
1333
+ e.preventDefault();
1334
+ void send(input);
1335
+ }
1336
+ }
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-end gap-2 px-2 pb-2", children: [
1340
+ busy && /* @__PURE__ */ jsxs4(
1341
+ Button,
1342
+ {
1343
+ type: "button",
1344
+ variant: "outline",
1345
+ size: "sm",
1346
+ className: "border-panel-border bg-transparent text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
1347
+ onClick: () => void stop(),
1348
+ children: [
1349
+ /* @__PURE__ */ jsx14(Square, {}),
1350
+ " Stop"
1351
+ ]
1352
+ }
1353
+ ),
1354
+ /* @__PURE__ */ jsxs4(
1355
+ Button,
1356
+ {
1357
+ type: "submit",
1358
+ size: "sm",
1359
+ disabled: busy || !input.trim(),
1360
+ className: "bg-brand text-brand-fg hover:bg-brand/90",
1361
+ children: [
1362
+ /* @__PURE__ */ jsx14(Send, {}),
1363
+ " Send"
1364
+ ]
1365
+ }
1366
+ )
1367
+ ] })
1368
+ ]
1369
+ }
1370
+ )
1371
+ ] })
1372
+ ] });
1373
+ }
1374
+
1375
+ // src/components/comments/CanvasComments.tsx
1376
+ import { useEditorMaybe } from "@grapesjs/react";
1377
+ import { useCallback, useEffect as useEffect3, useMemo, useRef as useRef2, useState as useState3 } from "react";
1378
+ import { Check as Check2, MessageSquarePlus, X as X2 } from "lucide-react";
1379
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs5 } from "react/jsx-runtime";
1380
+ var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
1381
+ var keyOfTarget = (t) => t.objectId ? `obj-${t.objectId}` : `sec-${t.sectionId}`;
1382
+ function CanvasComments({
1383
+ docId,
1384
+ composeTarget,
1385
+ onComposeConsumed,
1386
+ refreshSignal,
1387
+ onOpenCountChange
1388
+ }) {
1389
+ const commentStore = useCommentStore();
1390
+ const editor = useEditorMaybe();
1391
+ const containerRef = useRef2(null);
1392
+ const [comments, setComments] = useState3([]);
1393
+ const [positions, setPositions] = useState3({});
1394
+ const [activeKey, setActiveKey] = useState3(null);
1395
+ const [composeInfo, setComposeInfo] = useState3(null);
1396
+ const [body, setBody] = useState3("");
1397
+ const reload = useCallback(() => {
1398
+ commentStore.list(docId).then(setComments).catch(console.error);
1399
+ }, [docId]);
1400
+ useEffect3(() => {
1401
+ reload();
1402
+ }, [reload, refreshSignal]);
1403
+ const open = useMemo(() => comments.filter((c) => c.status === "open"), [comments]);
1404
+ useEffect3(() => {
1405
+ onOpenCountChange(open.length);
1406
+ }, [open.length, onOpenCountChange]);
1407
+ const pins = useMemo(() => {
1408
+ const map = /* @__PURE__ */ new Map();
1409
+ for (const c of open) {
1410
+ const k = keyOf(c);
1411
+ const arr = map.get(k) ?? [];
1412
+ arr.push(c);
1413
+ map.set(k, arr);
1414
+ }
1415
+ return [...map.entries()].map(([key, list]) => ({ key, list }));
1416
+ }, [open]);
1417
+ const openRef = useRef2(open);
1418
+ openRef.current = open;
1419
+ const activeRef = useRef2(activeKey);
1420
+ activeRef.current = activeKey;
1421
+ const positionsRef = useRef2({});
1422
+ const recompute = useCallback(() => {
1423
+ const ed = editor;
1424
+ const container = containerRef.current;
1425
+ if (!ed || !container) return;
1426
+ const frame = ed.Canvas.getFrameEl?.();
1427
+ const cdoc = ed.Canvas.getDocument?.();
1428
+ if (!frame || !cdoc) return;
1429
+ const cRect = container.getBoundingClientRect();
1430
+ const fRect = frame.getBoundingClientRect();
1431
+ const zoom = frame.offsetWidth ? fRect.width / frame.offsetWidth : 1;
1432
+ const keys = new Set(openRef.current.map(keyOf));
1433
+ if (activeRef.current) keys.add(activeRef.current);
1434
+ const next = {};
1435
+ keys.forEach((k) => {
1436
+ const el = cdoc.querySelector(`.${k}`);
1437
+ if (!el) return;
1438
+ const r = el.getBoundingClientRect();
1439
+ next[k] = {
1440
+ top: fRect.top - cRect.top + r.top * zoom + 4,
1441
+ left: fRect.left - cRect.left + (r.left + r.width) * zoom + 8
1442
+ };
1443
+ });
1444
+ if (JSON.stringify(positionsRef.current) !== JSON.stringify(next)) {
1445
+ positionsRef.current = next;
1446
+ setPositions(next);
1447
+ }
1448
+ }, [editor]);
1449
+ useEffect3(() => {
1450
+ const ed = editor;
1451
+ if (!ed) return;
1452
+ const evts = [
1453
+ "canvas:update",
1454
+ "update",
1455
+ "change:device",
1456
+ "load",
1457
+ "component:update",
1458
+ "component:mount"
1459
+ ];
1460
+ evts.forEach((e) => ed.on(e, recompute));
1461
+ const win = ed.Canvas.getWindow?.();
1462
+ win?.addEventListener("scroll", recompute, true);
1463
+ window.addEventListener("resize", recompute);
1464
+ const id = window.setInterval(recompute, 500);
1465
+ return () => {
1466
+ evts.forEach((e) => ed.off(e, recompute));
1467
+ win?.removeEventListener("scroll", recompute, true);
1468
+ window.removeEventListener("resize", recompute);
1469
+ window.clearInterval(id);
1470
+ };
1471
+ }, [editor, recompute]);
1472
+ useEffect3(() => {
1473
+ recompute();
1474
+ }, [comments, activeKey, recompute]);
1475
+ useEffect3(() => {
1476
+ if (!composeTarget) return;
1477
+ setComposeInfo(composeTarget);
1478
+ setActiveKey(keyOfTarget(composeTarget));
1479
+ setBody("");
1480
+ requestAnimationFrame(recompute);
1481
+ }, [composeTarget, recompute]);
1482
+ const activeComments = activeKey ? open.filter((c) => keyOf(c) === activeKey) : [];
1483
+ const activeTarget = (() => {
1484
+ if (!activeKey) return null;
1485
+ const c = activeComments[0];
1486
+ if (c) return { sectionId: c.sectionId, objectId: c.objectId, objectLabel: c.objectLabel };
1487
+ if (composeInfo && keyOfTarget(composeInfo) === activeKey) return composeInfo;
1488
+ return null;
1489
+ })();
1490
+ const activePos = activeKey ? positions[activeKey] : void 0;
1491
+ const lastAnchorPos = useRef2({ top: 0, left: 0 });
1492
+ if (activePos) lastAnchorPos.current = activePos;
1493
+ const anchorPos = activePos ?? lastAnchorPos.current;
1494
+ const closePopover = useCallback(() => {
1495
+ setActiveKey(null);
1496
+ setBody("");
1497
+ if (composeInfo) {
1498
+ setComposeInfo(null);
1499
+ onComposeConsumed();
1500
+ }
1501
+ }, [composeInfo, onComposeConsumed]);
1502
+ useEffect3(() => {
1503
+ if (!activeKey || !editor) return;
1504
+ const doc = editor.Canvas.getDocument?.();
1505
+ if (!doc) return;
1506
+ const onDown = () => closePopover();
1507
+ doc.addEventListener("mousedown", onDown, true);
1508
+ return () => doc.removeEventListener("mousedown", onDown, true);
1509
+ }, [activeKey, editor, closePopover]);
1510
+ const submit = async () => {
1511
+ if (!activeTarget || !body.trim()) return;
1512
+ try {
1513
+ await commentStore.add(docId, activeTarget, body.trim());
1514
+ setBody("");
1515
+ if (composeInfo) {
1516
+ setComposeInfo(null);
1517
+ onComposeConsumed();
1518
+ }
1519
+ reload();
1520
+ } catch (e) {
1521
+ console.error(e);
1522
+ }
1523
+ };
1524
+ const resolve = async (id) => {
1525
+ try {
1526
+ await commentStore.resolve(id);
1527
+ reload();
1528
+ } catch (e) {
1529
+ console.error(e);
1530
+ }
1531
+ };
1532
+ return /* @__PURE__ */ jsxs5("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
1533
+ pins.map(({ key, list }) => {
1534
+ const pos = positions[key];
1535
+ if (!pos) return null;
1536
+ return /* @__PURE__ */ jsx15(
1537
+ "button",
1538
+ {
1539
+ type: "button",
1540
+ onClick: () => {
1541
+ setActiveKey((k) => k === key ? null : key);
1542
+ setBody("");
1543
+ },
1544
+ style: { top: pos.top, left: pos.left },
1545
+ 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",
1546
+ title: `${list.length} comment(s)`,
1547
+ children: list.length
1548
+ },
1549
+ key
1550
+ );
1551
+ }),
1552
+ /* @__PURE__ */ jsxs5(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
1553
+ /* @__PURE__ */ jsx15(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx15(
1554
+ "div",
1555
+ {
1556
+ style: {
1557
+ position: "absolute",
1558
+ top: anchorPos.top,
1559
+ left: anchorPos.left,
1560
+ width: 1,
1561
+ height: 1
1562
+ }
1563
+ }
1564
+ ) }),
1565
+ /* @__PURE__ */ jsxs5(
1566
+ PopoverContent,
1567
+ {
1568
+ side: "right",
1569
+ align: "start",
1570
+ className: "pointer-events-auto w-80 p-0 data-[state=closed]:!animate-none data-[state=closed]:!duration-0",
1571
+ children: [
1572
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
1573
+ /* @__PURE__ */ jsx15("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1574
+ "Comment on",
1575
+ " ",
1576
+ /* @__PURE__ */ jsx15("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
1577
+ ] }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1578
+ "Comment on the whole section",
1579
+ " ",
1580
+ /* @__PURE__ */ jsx15("code", { className: "font-mono", children: activeTarget?.sectionId })
1581
+ ] }) }),
1582
+ /* @__PURE__ */ jsx15(
1583
+ "button",
1584
+ {
1585
+ type: "button",
1586
+ onClick: closePopover,
1587
+ "aria-label": "Close",
1588
+ className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
1589
+ children: /* @__PURE__ */ jsx15(X2, { className: "size-4" })
1590
+ }
1591
+ )
1592
+ ] }),
1593
+ /* @__PURE__ */ jsxs5("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
1594
+ activeComments.length === 0 && /* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "No comments yet \u2014 add the first one." }),
1595
+ activeComments.map((c) => /* @__PURE__ */ jsxs5("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
1596
+ /* @__PURE__ */ jsx15("p", { className: "whitespace-pre-wrap", children: c.body }),
1597
+ /* @__PURE__ */ jsxs5(
1598
+ "button",
1599
+ {
1600
+ onClick: () => void resolve(c.id),
1601
+ className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
1602
+ children: [
1603
+ /* @__PURE__ */ jsx15(Check2, { className: "size-3" }),
1604
+ " Resolve"
1605
+ ]
1606
+ }
1607
+ )
1608
+ ] }, c.id))
1609
+ ] }),
1610
+ /* @__PURE__ */ jsxs5("div", { className: "border-t p-3", children: [
1611
+ /* @__PURE__ */ jsx15(
1612
+ Textarea,
1613
+ {
1614
+ autoFocus: true,
1615
+ value: body,
1616
+ onChange: (e) => setBody(e.target.value),
1617
+ placeholder: "Add a comment\u2026 (\u2318/Ctrl+Enter to send)",
1618
+ className: "min-h-14 resize-none text-sm",
1619
+ onKeyDown: (e) => {
1620
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1621
+ e.preventDefault();
1622
+ void submit();
1623
+ }
1624
+ }
1625
+ }
1626
+ ),
1627
+ /* @__PURE__ */ jsx15("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ jsxs5(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
1628
+ /* @__PURE__ */ jsx15(MessageSquarePlus, {}),
1629
+ " Add"
1630
+ ] }) })
1631
+ ] })
1632
+ ]
1633
+ }
1634
+ )
1635
+ ] })
1636
+ ] });
1637
+ }
1638
+
1639
+ export {
1640
+ EditorStoreProvider,
1641
+ useDocumentStore,
1642
+ useCommentStore,
1643
+ STARTER_MJML,
1644
+ EditorHeader,
1645
+ Markdown,
1646
+ ChatPanel,
1647
+ CanvasComments
1648
+ };
1649
+ //# sourceMappingURL=chunk-LSF33PSW.js.map