@mjml-agent-editor/editor 0.1.11 → 0.1.14

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.
package/dist/canvas.cjs CHANGED
@@ -37,9 +37,9 @@ module.exports = __toCommonJS(canvas_exports);
37
37
 
38
38
  // src/components/editor/EmailEditor.tsx
39
39
  var import_grapesjs = __toESM(require("grapesjs"), 1);
40
- var import_react4 = __toESM(require("@grapesjs/react"), 1);
40
+ var import_react6 = __toESM(require("@grapesjs/react"), 1);
41
41
  var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
42
- var import_react5 = require("react");
42
+ var import_react7 = require("react");
43
43
  var import_core2 = require("@mjml-agent-editor/core");
44
44
 
45
45
  // src/stores.tsx
@@ -74,6 +74,29 @@ var DEFAULT_LABELS = {
74
74
  rteBackgroundColor: "Background color",
75
75
  documentLoadFailed: "Failed to load the document.",
76
76
  documentSaveFailed: "Failed to save changes.",
77
+ retry: "Retry",
78
+ chooseImage: "Choose image",
79
+ imagePickerTitle: "Choose an image",
80
+ imagePickerDrop: "Drag images here, or",
81
+ imagePickerBrowse: "Browse files",
82
+ imagePickerUrlPlaceholder: "\u2026or paste an image URL",
83
+ imagePickerUseUrl: "Use URL",
84
+ imagePickerGallery: "Your images",
85
+ imagePickerEmpty: "No images yet \u2014 the ones you upload will be kept here.",
86
+ imagePickerQuota: (used, limit) => `${used} of ${limit} uploads used`,
87
+ imagePickerQuotaFull: "Upload limit reached \u2014 pick an image below or paste a URL.",
88
+ imagePickerDelete: "Delete image",
89
+ imagePickerConfirmDelete: "Click again to delete",
90
+ imagePickerLoadFailed: "Failed to load your images.",
91
+ imageUploadFailed: "Failed to upload the image.",
92
+ imageDeleteFailed: "Failed to delete the image.",
93
+ imagePickerUnsupported: (fileName) => `${fileName} is not a supported image type.`,
94
+ imageFieldRemove: "Remove image",
95
+ colorPickerOpen: "Pick a color",
96
+ colorPickerEyedropper: "Pick from the screen",
97
+ colorPickerDocument: "In this email",
98
+ colorPickerPresets: "Presets",
99
+ colorPickerTransparent: "Transparent",
77
100
  commentOnSection: "Comment on the whole section",
78
101
  closeComments: "Close",
79
102
  noCommentsYet: "No comments yet \u2014 add the first one.",
@@ -115,6 +138,17 @@ function mergeLabels(overrides) {
115
138
  var import_jsx_runtime = require("react/jsx-runtime");
116
139
  var StoreContext = (0, import_react.createContext)(null);
117
140
  var LabelContext = (0, import_react.createContext)(null);
141
+ var TurnContext = (0, import_react.createContext)({
142
+ running: false,
143
+ setRunning: () => {
144
+ },
145
+ locked: false,
146
+ setLocked: () => {
147
+ }
148
+ });
149
+ function useAgentTurn() {
150
+ return (0, import_react.useContext)(TurnContext);
151
+ }
118
152
  function useLabels() {
119
153
  return (0, import_react.useContext)(LabelContext) ?? mergeLabels();
120
154
  }
@@ -133,6 +167,9 @@ function useDocumentStore() {
133
167
  function useCommentStore() {
134
168
  return useStores().comments;
135
169
  }
170
+ function useImageLibrary() {
171
+ return useStores().images;
172
+ }
136
173
 
137
174
  // src/index.ts
138
175
  var import_core = require("@mjml-agent-editor/core");
@@ -239,6 +276,9 @@ var import_jsx_runtime5 = require("react/jsx-runtime");
239
276
  function Popover({ ...props }) {
240
277
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Root, { "data-slot": "popover", ...props });
241
278
  }
279
+ function PopoverTrigger({ ...props }) {
280
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
281
+ }
242
282
  function PopoverAnchor({ ...props }) {
243
283
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Anchor, { "data-slot": "popover-anchor", ...props });
244
284
  }
@@ -263,11 +303,117 @@ function PopoverContent({
263
303
  ) });
264
304
  }
265
305
 
266
- // src/components/ui/textarea.tsx
306
+ // src/components/ui/dialog.tsx
267
307
  var React4 = require("react");
308
+ var import_radix_ui3 = require("radix-ui");
309
+ var import_lucide_react2 = require("lucide-react");
268
310
  var import_jsx_runtime6 = require("react/jsx-runtime");
269
- function Textarea({ className, ...props }) {
311
+ function Dialog({ ...props }) {
312
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Root, { "data-slot": "dialog", ...props });
313
+ }
314
+ function DialogPortal({ ...props }) {
315
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Portal, { "data-slot": "dialog-portal", ...props });
316
+ }
317
+ function DialogOverlay({
318
+ className,
319
+ ...props
320
+ }) {
321
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
322
+ import_radix_ui3.Dialog.Overlay,
323
+ {
324
+ "data-slot": "dialog-overlay",
325
+ className: cn(
326
+ "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",
327
+ className
328
+ ),
329
+ ...props
330
+ }
331
+ );
332
+ }
333
+ function DialogContent({
334
+ className,
335
+ children,
336
+ showCloseButton = true,
337
+ ...props
338
+ }) {
339
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DialogPortal, { children: [
340
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DialogOverlay, {}),
341
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
342
+ import_radix_ui3.Dialog.Content,
343
+ {
344
+ "data-slot": "dialog-content",
345
+ className: cn(
346
+ "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",
347
+ className
348
+ ),
349
+ ...props,
350
+ children: [
351
+ children,
352
+ showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm", children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react2.XIcon, {}),
354
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "sr-only", children: "Close" })
355
+ ] }) })
356
+ ]
357
+ }
358
+ )
359
+ ] });
360
+ }
361
+ function DialogHeader({ className, ...props }) {
362
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props });
363
+ }
364
+ function DialogTitle({ className, ...props }) {
365
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
366
+ import_radix_ui3.Dialog.Title,
367
+ {
368
+ "data-slot": "dialog-title",
369
+ className: cn("font-heading text-base leading-none font-medium", className),
370
+ ...props
371
+ }
372
+ );
373
+ }
374
+ function DialogDescription({
375
+ className,
376
+ ...props
377
+ }) {
270
378
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
379
+ import_radix_ui3.Dialog.Description,
380
+ {
381
+ "data-slot": "dialog-description",
382
+ className: cn(
383
+ "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
384
+ className
385
+ ),
386
+ ...props
387
+ }
388
+ );
389
+ }
390
+
391
+ // src/lib/refusal.ts
392
+ function refusal(error) {
393
+ if (!(error instanceof Error)) return null;
394
+ const status = error.status;
395
+ const deliberate = typeof status === "number" ? status >= 400 && status < 500 : null;
396
+ if (deliberate === false) return null;
397
+ if (error.message.startsWith("{")) {
398
+ try {
399
+ const parsed = JSON.parse(error.message);
400
+ return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+ const message = error.message.trim();
406
+ return deliberate && message !== "" && !message.startsWith("<") ? error.message : null;
407
+ }
408
+ function saveToastId(docId) {
409
+ return `document-save-${docId}`;
410
+ }
411
+
412
+ // src/components/ui/textarea.tsx
413
+ var React5 = require("react");
414
+ var import_jsx_runtime7 = require("react/jsx-runtime");
415
+ function Textarea({ className, ...props }) {
416
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
271
417
  "textarea",
272
418
  {
273
419
  "data-slot": "textarea",
@@ -283,8 +429,8 @@ function Textarea({ className, ...props }) {
283
429
  // src/components/comments/CanvasComments.tsx
284
430
  var import_react2 = require("@grapesjs/react");
285
431
  var import_react3 = require("react");
286
- var import_lucide_react2 = require("lucide-react");
287
- var import_jsx_runtime7 = require("react/jsx-runtime");
432
+ var import_lucide_react3 = require("lucide-react");
433
+ var import_jsx_runtime8 = require("react/jsx-runtime");
288
434
  var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
289
435
  var keyOfTarget = (t) => t.objectId ? `obj-${t.objectId}` : `sec-${t.sectionId}`;
290
436
  function CanvasComments({
@@ -438,11 +584,11 @@ function CanvasComments({
438
584
  console.error(e);
439
585
  }
440
586
  };
441
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
587
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
442
588
  pins.map(({ key, list }) => {
443
589
  const pos = positions[key];
444
590
  if (!pos) return null;
445
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
591
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
446
592
  "button",
447
593
  {
448
594
  type: "button",
@@ -458,8 +604,8 @@ function CanvasComments({
458
604
  key
459
605
  );
460
606
  }),
461
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
462
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
607
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
608
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
463
609
  "div",
464
610
  {
465
611
  style: {
@@ -471,45 +617,45 @@ function CanvasComments({
471
617
  }
472
618
  }
473
619
  ) }),
474
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
620
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
475
621
  PopoverContent,
476
622
  {
477
623
  side: "right",
478
624
  align: "start",
479
625
  className: "pointer-events-auto w-80 p-0 data-[state=closed]:!animate-none data-[state=closed]:!duration-0",
480
626
  children: [
481
- /* @__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: [
482
- /* @__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: [
627
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
628
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
483
629
  "Comment on",
484
630
  " ",
485
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
486
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
631
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
632
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
487
633
  labels.commentOnSection,
488
634
  " ",
489
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
635
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
490
636
  ] }) }),
491
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
637
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
492
638
  "button",
493
639
  {
494
640
  type: "button",
495
641
  onClick: closePopover,
496
642
  "aria-label": labels.closeComments,
497
643
  className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
498
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.X, { className: "size-4" })
644
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.X, { className: "size-4" })
499
645
  }
500
646
  )
501
647
  ] }),
502
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
503
- activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
504
- activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
505
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
506
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
648
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
649
+ activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
650
+ activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
652
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
507
653
  "button",
508
654
  {
509
655
  onClick: () => void resolve(c.id),
510
656
  className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
511
657
  children: [
512
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.Check, { className: "size-3" }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.Check, { className: "size-3" }),
513
659
  " ",
514
660
  labels.resolveComment
515
661
  ]
@@ -517,8 +663,8 @@ function CanvasComments({
517
663
  )
518
664
  ] }, c.id))
519
665
  ] }),
520
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "border-t p-3", children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
666
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "border-t p-3", children: [
667
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
522
668
  Textarea,
523
669
  {
524
670
  autoFocus: true,
@@ -534,8 +680,8 @@ function CanvasComments({
534
680
  }
535
681
  }
536
682
  ),
537
- /* @__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: [
538
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.MessageSquarePlus, {}),
683
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.MessageSquarePlus, {}),
539
685
  " ",
540
686
  labels.addComment
541
687
  ] }) })
@@ -547,21 +693,50 @@ function CanvasComments({
547
693
  ] });
548
694
  }
549
695
 
696
+ // src/lib/save-queue.ts
697
+ function createSaveQueue(save, delayMs) {
698
+ let timer = null;
699
+ let tail = Promise.resolve();
700
+ const cancel = () => {
701
+ if (timer) clearTimeout(timer);
702
+ timer = null;
703
+ };
704
+ const run = (task) => {
705
+ const next = tail.then(task);
706
+ tail = next.catch(() => {
707
+ });
708
+ return next;
709
+ };
710
+ return {
711
+ schedule() {
712
+ cancel();
713
+ timer = setTimeout(() => void run(save).catch(() => {
714
+ }), delayMs);
715
+ },
716
+ cancel,
717
+ flush() {
718
+ cancel();
719
+ return run(save);
720
+ },
721
+ run
722
+ };
723
+ }
724
+
550
725
  // src/components/editor/EmailEditor.tsx
551
- var import_sonner = require("sonner");
552
- var import_lucide_react3 = require("lucide-react");
726
+ var import_sonner2 = require("sonner");
727
+ var import_lucide_react7 = require("lucide-react");
553
728
 
554
729
  // src/components/ui/switch.tsx
555
- var React5 = require("react");
556
- var import_radix_ui3 = require("radix-ui");
557
- var import_jsx_runtime8 = require("react/jsx-runtime");
730
+ var React6 = require("react");
731
+ var import_radix_ui4 = require("radix-ui");
732
+ var import_jsx_runtime9 = require("react/jsx-runtime");
558
733
  function Switch({
559
734
  className,
560
735
  size = "default",
561
736
  ...props
562
737
  }) {
563
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
564
- import_radix_ui3.Switch.Root,
738
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
739
+ import_radix_ui4.Switch.Root,
565
740
  {
566
741
  "data-slot": "switch",
567
742
  "data-size": size,
@@ -570,8 +745,8 @@ function Switch({
570
745
  className
571
746
  ),
572
747
  ...props,
573
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
574
- import_radix_ui3.Switch.Thumb,
748
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
749
+ import_radix_ui4.Switch.Thumb,
575
750
  {
576
751
  "data-slot": "switch-thumb",
577
752
  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"
@@ -582,8 +757,583 @@ function Switch({
582
757
  }
583
758
 
584
759
  // src/components/editor/EmailEditor.tsx
760
+ var import_lucide_react8 = require("lucide-react");
761
+
762
+ // src/components/editor/ImagePicker.tsx
763
+ var import_react4 = require("react");
585
764
  var import_lucide_react4 = require("lucide-react");
586
- var import_jsx_runtime9 = require("react/jsx-runtime");
765
+ var import_sonner = require("sonner");
766
+ var import_jsx_runtime10 = require("react/jsx-runtime");
767
+ function ImagePicker({
768
+ open,
769
+ currentUrl,
770
+ onSelect,
771
+ onClose
772
+ }) {
773
+ const library = useImageLibrary();
774
+ const labels = useLabels();
775
+ const [images, setImages] = (0, import_react4.useState)([]);
776
+ const [quota, setQuota] = (0, import_react4.useState)();
777
+ const [loading, setLoading] = (0, import_react4.useState)(false);
778
+ const [pending, setPending] = (0, import_react4.useState)(0);
779
+ const [dragging, setDragging] = (0, import_react4.useState)(false);
780
+ const [url, setUrl] = (0, import_react4.useState)("");
781
+ const [confirmId, setConfirmId] = (0, import_react4.useState)(null);
782
+ const fileInput = (0, import_react4.useRef)(null);
783
+ const session = (0, import_react4.useRef)(0);
784
+ const latest = (0, import_react4.useRef)({ labels, onSelect });
785
+ latest.current = { labels, onSelect };
786
+ const lists = (0, import_react4.useRef)(0);
787
+ const refresh = (0, import_react4.useCallback)(async () => {
788
+ if (!library) return;
789
+ const mine = ++lists.current;
790
+ setLoading(true);
791
+ try {
792
+ const result = await library.list();
793
+ if (mine !== lists.current) return;
794
+ setImages(result.images);
795
+ setQuota(result.quota);
796
+ } catch (err) {
797
+ console.error(err);
798
+ if (mine === lists.current) import_sonner.toast.error(latest.current.labels.imagePickerLoadFailed);
799
+ } finally {
800
+ if (mine === lists.current) setLoading(false);
801
+ }
802
+ }, [library]);
803
+ (0, import_react4.useEffect)(() => {
804
+ session.current += 1;
805
+ if (!open) return;
806
+ setUrl("");
807
+ setConfirmId(null);
808
+ void refresh();
809
+ }, [open]);
810
+ (0, import_react4.useEffect)(() => () => void (session.current += 1), []);
811
+ const accepts = (file) => matchesAccept(file, library?.accept);
812
+ const full = quota !== void 0 && quota.used >= quota.limit;
813
+ const upload = async (files) => {
814
+ if (!library || !files?.length) return;
815
+ const mine = session.current;
816
+ const list = Array.from(files).filter((f) => {
817
+ if (accepts(f)) return true;
818
+ import_sonner.toast.error(labels.imagePickerUnsupported(f.name));
819
+ return false;
820
+ });
821
+ if (!list.length) return;
822
+ setPending((n) => n + list.length);
823
+ const results = await Promise.allSettled(list.map(async (f) => library.upload(f)));
824
+ setPending((n) => n - list.length);
825
+ for (const r of results) {
826
+ if (r.status === "rejected") {
827
+ console.error(r.reason);
828
+ import_sonner.toast.error(r.reason instanceof Error ? r.reason.message : labels.imageUploadFailed);
829
+ }
830
+ }
831
+ if (session.current !== mine) return;
832
+ await refresh();
833
+ const [only] = results;
834
+ if (results.length === 1 && only?.status === "fulfilled" && session.current === mine) {
835
+ latest.current.onSelect(only.value.url);
836
+ }
837
+ };
838
+ const remove = async (image) => {
839
+ if (!library?.remove) return;
840
+ if (confirmId !== image.id) {
841
+ setConfirmId(image.id);
842
+ return;
843
+ }
844
+ setConfirmId(null);
845
+ try {
846
+ await library.remove(image.id);
847
+ await refresh();
848
+ } catch (err) {
849
+ console.error(err);
850
+ import_sonner.toast.error(err instanceof Error ? err.message : labels.imageDeleteFailed);
851
+ }
852
+ };
853
+ const onDrop = (e) => {
854
+ e.preventDefault();
855
+ setDragging(false);
856
+ if (!full) void upload(e.dataTransfer.files);
857
+ };
858
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogContent, { className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl", children: [
859
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
860
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
861
+ quota && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
862
+ ] }),
863
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
864
+ library && (full ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
865
+ "div",
866
+ {
867
+ onDragOver: (e) => {
868
+ e.preventDefault();
869
+ setDragging(true);
870
+ },
871
+ onDragLeave: () => setDragging(false),
872
+ onDrop,
873
+ className: cn(
874
+ "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
875
+ dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
876
+ ),
877
+ children: [
878
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
880
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: labels.imagePickerDrop }),
881
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
882
+ Button,
883
+ {
884
+ type: "button",
885
+ size: "sm",
886
+ className: "bg-brand text-brand-fg hover:bg-brand/90",
887
+ onClick: () => fileInput.current?.click(),
888
+ children: labels.imagePickerBrowse
889
+ }
890
+ )
891
+ ] }),
892
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
893
+ "input",
894
+ {
895
+ ref: fileInput,
896
+ type: "file",
897
+ accept: library.accept ?? "image/*",
898
+ multiple: true,
899
+ hidden: true,
900
+ onChange: (e) => {
901
+ void upload(e.target.files);
902
+ e.target.value = "";
903
+ }
904
+ }
905
+ )
906
+ ]
907
+ }
908
+ )),
909
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
910
+ "form",
911
+ {
912
+ className: "flex gap-2",
913
+ onSubmit: (e) => {
914
+ e.preventDefault();
915
+ const value = url.trim();
916
+ if (value) onSelect(value);
917
+ },
918
+ children: [
919
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative flex-1", children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
921
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
922
+ Input,
923
+ {
924
+ value: url,
925
+ onChange: (e) => setUrl(e.target.value),
926
+ placeholder: labels.imagePickerUrlPlaceholder,
927
+ className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
928
+ }
929
+ )
930
+ ] }),
931
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
932
+ Button,
933
+ {
934
+ type: "submit",
935
+ size: "lg",
936
+ variant: "outline",
937
+ disabled: !url.trim(),
938
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
939
+ children: labels.imagePickerUseUrl
940
+ }
941
+ )
942
+ ]
943
+ }
944
+ ),
945
+ library && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { children: [
946
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
947
+ loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
948
+ Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
949
+ "li",
950
+ {
951
+ className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
952
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {})
953
+ },
954
+ `pending-${i}`
955
+ )),
956
+ images.map((image) => {
957
+ const selected = image.url === currentUrl;
958
+ const confirming = confirmId === image.id;
959
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { className: "group relative", children: [
960
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
961
+ "button",
962
+ {
963
+ type: "button",
964
+ onClick: () => onSelect(image.url),
965
+ title: image.name,
966
+ className: cn(
967
+ "block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
968
+ selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
969
+ ),
970
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
971
+ "img",
972
+ {
973
+ src: image.url,
974
+ alt: image.name ?? "",
975
+ loading: "lazy",
976
+ className: "size-full object-contain"
977
+ }
978
+ )
979
+ }
980
+ ),
981
+ selected && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
982
+ image.name && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
983
+ library.remove && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
984
+ "button",
985
+ {
986
+ type: "button",
987
+ onClick: () => void remove(image),
988
+ onBlur: () => confirming && setConfirmId(null),
989
+ title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
990
+ "aria-label": labels.imagePickerDelete,
991
+ className: cn(
992
+ "absolute top-1.5 right-1.5 flex h-6 items-center gap-1 rounded-md px-1.5 text-xs text-white transition focus-visible:opacity-100",
993
+ confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
994
+ ),
995
+ children: [
996
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
997
+ confirming && labels.imagePickerConfirmDelete
998
+ ]
999
+ }
1000
+ )
1001
+ ] }, image.id);
1002
+ })
1003
+ ] })
1004
+ ] })
1005
+ ] })
1006
+ ] }) });
1007
+ }
1008
+ function matchesAccept(file, accept) {
1009
+ if (!accept) return file.type.startsWith("image/");
1010
+ return accept.split(",").some((raw) => {
1011
+ const rule = raw.trim().toLowerCase();
1012
+ if (rule.startsWith(".")) return file.name.toLowerCase().endsWith(rule);
1013
+ if (rule.endsWith("/*")) return file.type.startsWith(rule.slice(0, -1));
1014
+ return file.type === rule;
1015
+ });
1016
+ }
1017
+
1018
+ // src/components/editor/ColorField.tsx
1019
+ var import_react5 = require("react");
1020
+ var import_react_colorful = require("react-colorful");
1021
+ var import_lucide_react5 = require("lucide-react");
1022
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1023
+ var PRESETS = [
1024
+ "#000000",
1025
+ "#374151",
1026
+ "#6b7280",
1027
+ "#d1d5db",
1028
+ "#f3f4f6",
1029
+ "#ffffff",
1030
+ "#dc2626",
1031
+ "#ea580c",
1032
+ "#ca8a04",
1033
+ "#16a34a",
1034
+ "#0284c7",
1035
+ "#4f46e5",
1036
+ "#9333ea",
1037
+ "#db2777"
1038
+ ];
1039
+ var eyeDropper = () => globalThis.EyeDropper;
1040
+ function ColorField({
1041
+ value,
1042
+ placeholder,
1043
+ documentColors,
1044
+ labels,
1045
+ onChange
1046
+ }) {
1047
+ const [text, setText] = (0, import_react5.useState)(value);
1048
+ const [open, setOpen] = (0, import_react5.useState)(false);
1049
+ const [used, setUsed] = (0, import_react5.useState)([]);
1050
+ const pressed = (0, import_react5.useRef)(false);
1051
+ const dragged = (0, import_react5.useRef)(null);
1052
+ const fieldRef = (0, import_react5.useRef)(null);
1053
+ const anchor = (0, import_react5.useRef)({
1054
+ getBoundingClientRect: () => {
1055
+ const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
1056
+ const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
1057
+ return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
1058
+ }
1059
+ });
1060
+ (0, import_react5.useEffect)(() => setText(value), [value]);
1061
+ const latestOnChange = (0, import_react5.useRef)(onChange);
1062
+ latestOnChange.current = onChange;
1063
+ const release = (0, import_react5.useCallback)(() => {
1064
+ pressed.current = false;
1065
+ if (dragged.current !== null) latestOnChange.current(dragged.current, false);
1066
+ dragged.current = null;
1067
+ }, []);
1068
+ (0, import_react5.useEffect)(() => {
1069
+ document.addEventListener("lostpointercapture", release);
1070
+ return () => {
1071
+ document.removeEventListener("lostpointercapture", release);
1072
+ release();
1073
+ };
1074
+ }, [release]);
1075
+ const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
1076
+ const translucent = hex.length === 9;
1077
+ const empty = !value;
1078
+ const commit = (next) => {
1079
+ dragged.current = null;
1080
+ onChange(next, false);
1081
+ };
1082
+ const drag = (next) => {
1083
+ setText(next);
1084
+ if (!pressed.current) return commit(next);
1085
+ dragged.current = next;
1086
+ onChange(next, true);
1087
+ };
1088
+ const pickFromScreen = async () => {
1089
+ const Ctor = eyeDropper();
1090
+ if (!Ctor) return;
1091
+ try {
1092
+ const { sRGBHex } = await new Ctor().open();
1093
+ commit(sRGBHex);
1094
+ } catch {
1095
+ }
1096
+ };
1097
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1098
+ Popover,
1099
+ {
1100
+ open,
1101
+ onOpenChange: (next) => {
1102
+ if (next) setUsed(documentColors());
1103
+ else if (text.trim() !== value) commit(text.trim());
1104
+ setOpen(next);
1105
+ },
1106
+ children: [
1107
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverAnchor, { virtualRef: anchor }),
1108
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1109
+ "div",
1110
+ {
1111
+ ref: fieldRef,
1112
+ className: "flex h-8 w-full items-center gap-1.5 rounded-md border border-panel-border bg-panel-elevated pr-1.5 pl-2 focus-within:border-brand",
1113
+ children: [
1114
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1115
+ "input",
1116
+ {
1117
+ value: text,
1118
+ placeholder,
1119
+ spellCheck: false,
1120
+ onChange: (e) => setText(e.target.value),
1121
+ onBlur: () => text !== value && commit(text.trim()),
1122
+ onKeyDown: (e) => {
1123
+ if (e.key === "Enter") commit(text.trim());
1124
+ if (e.key === "Escape") setText(value);
1125
+ },
1126
+ className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
1127
+ }
1128
+ ),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1130
+ "button",
1131
+ {
1132
+ type: "button",
1133
+ title: labels.colorPickerOpen,
1134
+ "aria-label": labels.colorPickerOpen,
1135
+ className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
1136
+ children: [
1137
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {}),
1138
+ !empty && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "absolute inset-0", style: { background: value } })
1139
+ ]
1140
+ }
1141
+ ) })
1142
+ ]
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1146
+ PopoverContent,
1147
+ {
1148
+ side: "right",
1149
+ align: "start",
1150
+ sideOffset: 8,
1151
+ className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
1152
+ children: [
1153
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1154
+ "div",
1155
+ {
1156
+ onPointerDownCapture: (e) => {
1157
+ pressed.current = true;
1158
+ e.target.setPointerCapture(e.pointerId);
1159
+ },
1160
+ children: translucent ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
1161
+ }
1162
+ ),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1164
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1165
+ "span",
1166
+ {
1167
+ className: "size-6 shrink-0 rounded border border-white/20",
1168
+ style: { background: hex }
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1172
+ "input",
1173
+ {
1174
+ value: text,
1175
+ spellCheck: false,
1176
+ onChange: (e) => setText(e.target.value),
1177
+ onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
1178
+ onBlur: () => text !== value && commit(text.trim()),
1179
+ className: "h-6 min-w-0 flex-1 rounded border border-panel-border bg-panel-elevated px-1.5 font-mono text-[11px] outline-none focus:border-brand"
1180
+ }
1181
+ ),
1182
+ eyeDropper() && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1183
+ "button",
1184
+ {
1185
+ type: "button",
1186
+ onClick: () => void pickFromScreen(),
1187
+ title: labels.colorPickerEyedropper,
1188
+ "aria-label": labels.colorPickerEyedropper,
1189
+ className: "flex size-6 shrink-0 items-center justify-center rounded border border-panel-border bg-panel-elevated text-panel-muted-fg hover:text-panel-fg",
1190
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react5.Pipette, { className: "size-3" })
1191
+ }
1192
+ )
1193
+ ] }),
1194
+ used.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1195
+ Swatches,
1196
+ {
1197
+ title: labels.colorPickerDocument,
1198
+ colors: used,
1199
+ current: hex,
1200
+ onPick: commit
1201
+ }
1202
+ ),
1203
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1204
+ Swatches,
1205
+ {
1206
+ title: labels.colorPickerPresets,
1207
+ colors: PRESETS,
1208
+ current: hex,
1209
+ onPick: commit,
1210
+ transparentLabel: labels.colorPickerTransparent
1211
+ }
1212
+ )
1213
+ ]
1214
+ }
1215
+ )
1216
+ ]
1217
+ }
1218
+ );
1219
+ }
1220
+ function Swatches({
1221
+ title,
1222
+ colors,
1223
+ current,
1224
+ onPick,
1225
+ transparentLabel
1226
+ }) {
1227
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
1228
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
1229
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "grid grid-cols-8 gap-1", children: [
1230
+ colors.map((color) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1231
+ "button",
1232
+ {
1233
+ type: "button",
1234
+ title: color,
1235
+ onClick: () => onPick(color),
1236
+ className: cn(
1237
+ "aspect-square rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
1238
+ color.toLowerCase() === current.toLowerCase() && "ring-2 ring-brand"
1239
+ ),
1240
+ style: { background: color }
1241
+ },
1242
+ color
1243
+ )),
1244
+ transparentLabel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1245
+ "button",
1246
+ {
1247
+ type: "button",
1248
+ title: transparentLabel,
1249
+ onClick: () => onPick("transparent"),
1250
+ className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
1251
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {})
1252
+ }
1253
+ )
1254
+ ] })
1255
+ ] });
1256
+ }
1257
+ function Checkerboard() {
1258
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1259
+ "span",
1260
+ {
1261
+ className: "absolute inset-0",
1262
+ style: {
1263
+ backgroundImage: "repeating-conic-gradient(#9ca3af 0% 25%, #e5e7eb 0% 50%)",
1264
+ backgroundSize: "8px 8px"
1265
+ }
1266
+ }
1267
+ );
1268
+ }
1269
+ function toHex(value, { opaque = false } = {}) {
1270
+ const v = value.trim().toLowerCase();
1271
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
1272
+ if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
1273
+ if (/^#[0-9a-f]{6}$/.test(v)) return v;
1274
+ if (/^#[0-9a-f]{8}$/.test(v)) return opaque || v.endsWith("ff") ? v.slice(0, 7) : v;
1275
+ const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?/.exec(v);
1276
+ if (rgb) {
1277
+ const channels = [rgb[1], rgb[2], rgb[3]].map((n) => Math.min(255, Number(n)));
1278
+ const alphaText = rgb[4];
1279
+ const alpha = alphaText?.endsWith("%") ? parseFloat(alphaText) / 100 : Number(alphaText ?? 1);
1280
+ if (!opaque && alpha < 1) channels.push(Math.round(Math.max(0, alpha) * 255));
1281
+ return `#${channels.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
1282
+ }
1283
+ return null;
1284
+ }
1285
+
1286
+ // src/components/editor/ImageField.tsx
1287
+ var import_lucide_react6 = require("lucide-react");
1288
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1289
+ function ImageField({
1290
+ value,
1291
+ labels,
1292
+ onChoose,
1293
+ onClear
1294
+ }) {
1295
+ const url = imageUrl(value);
1296
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "relative", children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1298
+ "button",
1299
+ {
1300
+ type: "button",
1301
+ onClick: onChoose,
1302
+ title: labels.chooseImage,
1303
+ className: "group relative block w-full overflow-hidden rounded-md border border-panel-border bg-panel-elevated outline-none focus-visible:ring-2 focus-visible:ring-brand",
1304
+ children: [
1305
+ url ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
1306
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.ImageIcon, { className: "size-5" }),
1307
+ labels.chooseImage
1308
+ ] }),
1309
+ url && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
1310
+ ]
1311
+ }
1312
+ ),
1313
+ value && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1314
+ "button",
1315
+ {
1316
+ type: "button",
1317
+ onClick: onClear,
1318
+ title: labels.imageFieldRemove,
1319
+ "aria-label": labels.imageFieldRemove,
1320
+ className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
1321
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.X, { className: "size-3" })
1322
+ }
1323
+ )
1324
+ ] });
1325
+ }
1326
+ function imageUrl(value) {
1327
+ const v = value.trim();
1328
+ if (!v || v === "none") return null;
1329
+ const css = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(v);
1330
+ if (css) return css[2] || null;
1331
+ return /^[a-z-]+\(/i.test(v) ? null : v;
1332
+ }
1333
+
1334
+ // src/components/editor/EmailEditor.tsx
1335
+ var import_client = require("react-dom/client");
1336
+ var import_jsx_runtime13 = require("react/jsx-runtime");
587
1337
  var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
588
1338
  var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
589
1339
  var TYPE_LABEL = {
@@ -596,6 +1346,7 @@ function tagOf(c) {
596
1346
  return String(c.get("tagName") || c.get("type") || "");
597
1347
  }
598
1348
  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>';
1349
+ var IMAGE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"/><path d="m14 19.5 3-3 3 3"/><path d="M17 22v-5.5"/><circle cx="9" cy="9" r="2"/></svg>';
599
1350
  function isSection(c) {
600
1351
  return tagOf(c) === "mj-section";
601
1352
  }
@@ -678,6 +1429,75 @@ function wrapSelectionStyle(el, style) {
678
1429
  } catch {
679
1430
  }
680
1431
  }
1432
+ function setupStyleFields(editor, labels) {
1433
+ const sm = editor.StyleManager;
1434
+ const base = sm.getType("base").view;
1435
+ const documentColors = () => {
1436
+ const source = `${editor.getHtml()} ${editor.getCss() ?? ""}`;
1437
+ const counts = /* @__PURE__ */ new Map();
1438
+ const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
1439
+ for (const match of source.matchAll(colorValue)) {
1440
+ const hex = toHex(match[1], { opaque: true });
1441
+ if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
1442
+ }
1443
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
1444
+ };
1445
+ const ownValue = (property) => property.hasValue?.({ noParent: true }) ? String(property.getValue?.() ?? "") : "";
1446
+ const reactView = (field) => base.extend({
1447
+ create({ property }) {
1448
+ const el = document.createElement("div");
1449
+ el.addEventListener("change", (event) => event.stopPropagation());
1450
+ const root = (0, import_client.createRoot)(el);
1451
+ this.__root = root;
1452
+ this.__render = () => root.render(field(property, ownValue(property)));
1453
+ this.__render();
1454
+ return el;
1455
+ },
1456
+ update() {
1457
+ this.__render?.();
1458
+ },
1459
+ destroy() {
1460
+ const root = this.__root;
1461
+ this.__root = void 0;
1462
+ this.__render = void 0;
1463
+ setTimeout(() => root?.unmount());
1464
+ }
1465
+ });
1466
+ sm.addType("color", {
1467
+ view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1468
+ ColorField,
1469
+ {
1470
+ value,
1471
+ placeholder: String(property.getDefaultValue?.() ?? ""),
1472
+ documentColors,
1473
+ labels,
1474
+ onChange: (next, partial) => property.upValue(next, { partial })
1475
+ }
1476
+ ))
1477
+ });
1478
+ sm.addType("file", {
1479
+ view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1480
+ ImageField,
1481
+ {
1482
+ value,
1483
+ labels,
1484
+ onClear: () => property.upValue(""),
1485
+ onChoose: () => {
1486
+ const am = editor.AssetManager;
1487
+ am.open({
1488
+ types: ["image"],
1489
+ current: imageUrl(value) ?? void 0,
1490
+ select(asset, complete) {
1491
+ const url = typeof asset === "string" ? asset : asset.getSrc();
1492
+ property.upValue(url, { partial: !complete });
1493
+ if (complete) am.close();
1494
+ }
1495
+ });
1496
+ }
1497
+ }
1498
+ ))
1499
+ });
1500
+ }
681
1501
  function setupRichText(editor, labels) {
682
1502
  const rte = editor.RichTextEditor;
683
1503
  if (!rte || rte.__customActions) return;
@@ -717,16 +1537,25 @@ function setupRichText(editor, labels) {
717
1537
  function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
718
1538
  const documents = useDocumentStore();
719
1539
  const labels = useLabels();
720
- const loadingRef = (0, import_react5.useRef)(false);
721
- const saveTimer = (0, import_react5.useRef)(null);
722
- const saveStatusRef = (0, import_react5.useRef)("idle");
723
- const stateListeners = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
724
- const [loading, setLoading] = (0, import_react5.useState)(true);
725
- const [sidebarView, setSidebarView] = (0, import_react5.useState)("blocks");
726
- const [composeTarget, setComposeTarget] = (0, import_react5.useState)(null);
727
- const selectionSource = (0, import_react5.useRef)("other");
728
- const editorRef = (0, import_react5.useRef)(null);
729
- const [crumbs, setCrumbs] = (0, import_react5.useState)([]);
1540
+ const loadingRef = (0, import_react7.useRef)(false);
1541
+ const saveStatusRef = (0, import_react7.useRef)("idle");
1542
+ const stateListeners = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
1543
+ const [loading, setLoading] = (0, import_react7.useState)(true);
1544
+ const [sidebarView, setSidebarView] = (0, import_react7.useState)("blocks");
1545
+ const [composeTarget, setComposeTarget] = (0, import_react7.useState)(null);
1546
+ const selectionSource = (0, import_react7.useRef)("other");
1547
+ const editorRef = (0, import_react7.useRef)(null);
1548
+ const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
1549
+ const readOnlyRef = (0, import_react7.useRef)(false);
1550
+ const applyReadOnly = (0, import_react7.useRef)(async () => {
1551
+ });
1552
+ (0, import_react7.useEffect)(() => void applyReadOnly.current(turnRunning), [turnRunning]);
1553
+ (0, import_react7.useEffect)(() => () => setLocked(false), [setLocked]);
1554
+ const [crumbs, setCrumbs] = (0, import_react7.useState)([]);
1555
+ const [selectedImageSrc, setSelectedImageSrc] = (0, import_react7.useState)(null);
1556
+ const [picker, setPicker] = (0, import_react7.useState)(
1557
+ null
1558
+ );
730
1559
  const onEditor = async (editor) => {
731
1560
  editorRef.current = editor;
732
1561
  const buildCrumbs = (c) => {
@@ -753,11 +1582,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
753
1582
  };
754
1583
  const snapshot = () => ({
755
1584
  device: editor.getDevice(),
756
- canUndo: editor.UndoManager.hasUndo(),
757
- canRedo: editor.UndoManager.hasRedo(),
1585
+ canUndo: !readOnlyRef.current && editor.UndoManager.hasUndo(),
1586
+ canRedo: !readOnlyRef.current && editor.UndoManager.hasRedo(),
758
1587
  saveStatus: saveStatusRef.current,
759
1588
  zoom: Math.round(editor.Canvas.getZoom?.() ?? 100),
760
- contentWidth: bodyWidth()
1589
+ contentWidth: bodyWidth(),
1590
+ readOnly: readOnlyRef.current
761
1591
  });
762
1592
  const notifyState = () => {
763
1593
  const s = snapshot();
@@ -781,6 +1611,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
781
1611
  if (isSection(c)) addIdClass(c, "sec");
782
1612
  if (!isCommentable(c)) return;
783
1613
  const toolbar = [...c.get("toolbar") ?? []];
1614
+ if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
1615
+ toolbar.push({
1616
+ command: "open-image-picker",
1617
+ label: IMAGE_SVG,
1618
+ attributes: { title: labels.chooseImage }
1619
+ });
1620
+ c.set("toolbar", toolbar);
1621
+ }
784
1622
  if (!toolbar.some((t) => t.command === "open-comments")) {
785
1623
  toolbar.push({
786
1624
  command: "open-comments",
@@ -790,6 +1628,21 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
790
1628
  c.set("toolbar", toolbar);
791
1629
  }
792
1630
  };
1631
+ editor.Commands.add("open-image-picker", {
1632
+ run(ed) {
1633
+ const target = ed.getSelected();
1634
+ if (!target || tagOf(target) !== "mj-image") return;
1635
+ const am = ed.AssetManager;
1636
+ am.open({
1637
+ target,
1638
+ types: ["image"],
1639
+ select(asset, complete) {
1640
+ target.set({ src: asset.getSrc() });
1641
+ if (complete) am.close();
1642
+ }
1643
+ });
1644
+ }
1645
+ });
793
1646
  editor.Commands.add("open-comments", {
794
1647
  run(ed) {
795
1648
  const target = targetOf(ed.getSelected() ?? void 0);
@@ -797,6 +1650,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
797
1650
  }
798
1651
  });
799
1652
  setupRichText(editor, labels);
1653
+ setupStyleFields(editor, labels);
800
1654
  const blockCategory = (label) => {
801
1655
  const l = label.toLowerCase();
802
1656
  if (/(column|section)/.test(l)) return "Layout";
@@ -825,6 +1679,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
825
1679
  if (selectionSource.current === "canvas") setSidebarView("settings");
826
1680
  selectionSource.current = "other";
827
1681
  buildCrumbs(c);
1682
+ setSelectedImageSrc(c && tagOf(c) === "mj-image" ? String(c.get("src") ?? "") : null);
828
1683
  if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
829
1684
  c.addTrait(
830
1685
  {
@@ -838,40 +1693,99 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
838
1693
  );
839
1694
  }
840
1695
  });
841
- editor.on("component:deselected", () => setCrumbs([]));
1696
+ editor.on("component:deselected", () => {
1697
+ const rest = editor.getSelected();
1698
+ buildCrumbs(rest);
1699
+ setSelectedImageSrc(
1700
+ rest && tagOf(rest) === "mj-image" ? String(rest.get("src") ?? "") : null
1701
+ );
1702
+ });
1703
+ editor.on("component:update:src", (c) => {
1704
+ if (c === editor.getSelected()) setSelectedImageSrc(String(c.get("src") ?? ""));
1705
+ });
1706
+ let stored = "";
842
1707
  const save = async () => {
843
- if (loadingRef.current) return;
1708
+ const view = editor.getEditing()?.getView();
1709
+ await view?.syncContent?.();
1710
+ const mjml = editor.getHtml();
1711
+ if (mjml === stored) return;
844
1712
  setSave("saving");
845
1713
  try {
846
- await documents.save(docId, {
847
- mjml: editor.getHtml(),
848
- projectData: editor.getProjectData()
849
- });
1714
+ await documents.save(docId, { mjml, projectData: editor.getProjectData() });
1715
+ stored = mjml;
850
1716
  setSave("saved");
851
1717
  } catch (e) {
852
1718
  setSave("error");
853
- import_sonner.toast.error(labels.documentSaveFailed);
1719
+ import_sonner2.toast.error(refusal(e) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
854
1720
  throw e;
855
1721
  }
856
1722
  };
1723
+ const queue = createSaveQueue(save, 1200);
857
1724
  editor.on("update", () => {
858
1725
  notifyState();
859
- if (loadingRef.current) return;
860
- if (saveTimer.current) clearTimeout(saveTimer.current);
861
- saveTimer.current = setTimeout(() => void save().catch(console.error), 1200);
1726
+ if (!loadingRef.current && !readOnlyRef.current) queue.schedule();
862
1727
  });
863
1728
  editor.on("change:device", notifyState);
1729
+ const blocked = [
1730
+ "core:undo",
1731
+ "core:redo",
1732
+ "core:paste",
1733
+ "core:component-delete",
1734
+ "tlb-delete",
1735
+ "tlb-clone",
1736
+ "tlb-move",
1737
+ "open-assets",
1738
+ "open-image-picker"
1739
+ ];
1740
+ for (const id of blocked) {
1741
+ editor.on(`command:run:before:${id}`, ({ options }) => {
1742
+ if (readOnlyRef.current) options.abort = true;
1743
+ });
1744
+ }
1745
+ const lock = (on) => {
1746
+ readOnlyRef.current = on;
1747
+ setLocked(on);
1748
+ notifyState();
1749
+ };
1750
+ applyReadOnly.current = async (on) => {
1751
+ if (on) {
1752
+ lock(true);
1753
+ queue.cancel();
1754
+ editor.select([]);
1755
+ editor.AssetManager.close();
1756
+ setPicker(null);
1757
+ return;
1758
+ }
1759
+ try {
1760
+ await reload();
1761
+ } catch (e) {
1762
+ console.error(e);
1763
+ import_sonner2.toast.error(labels.documentLoadFailed, {
1764
+ id: `reload-${docId}`,
1765
+ duration: Infinity,
1766
+ action: { label: labels.retry, onClick: () => void applyReadOnly.current(false) }
1767
+ });
1768
+ return;
1769
+ }
1770
+ lock(false);
1771
+ };
1772
+ const reload = () => queue.run(async () => {
1773
+ const fresh = await documents.get(docId);
1774
+ if (fresh.mjml !== stored) loadMjml(fresh.mjml);
1775
+ });
864
1776
  const loadMjml = (mjml) => {
865
1777
  loadingRef.current = true;
866
- if (saveTimer.current) clearTimeout(saveTimer.current);
867
- editor.setComponents(mjml || import_core2.STARTER_MJML);
868
- editor.getWrapper()?.find("mj-section").forEach(decorate);
869
- for (const t of Object.keys(TYPE_LABEL)) {
870
- editor.getWrapper()?.find(t).forEach(decorate);
871
- }
872
- setTimeout(() => {
1778
+ try {
1779
+ queue.cancel();
1780
+ stored = mjml;
1781
+ editor.setComponents(mjml || import_core2.STARTER_MJML);
1782
+ editor.getWrapper()?.find("mj-section").forEach(decorate);
1783
+ for (const t of Object.keys(TYPE_LABEL)) {
1784
+ editor.getWrapper()?.find(t).forEach(decorate);
1785
+ }
1786
+ } finally {
873
1787
  loadingRef.current = false;
874
- }, 400);
1788
+ }
875
1789
  };
876
1790
  const ensureHighlightStyles = () => {
877
1791
  const cdoc = editor.Canvas.getDocument();
@@ -908,7 +1822,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
908
1822
  const doc = await documents.get(docId);
909
1823
  loadMjml(doc.mjml);
910
1824
  } catch (e) {
911
- import_sonner.toast.error(labels.documentLoadFailed);
1825
+ import_sonner2.toast.error(labels.documentLoadFailed);
912
1826
  console.error(e);
913
1827
  } finally {
914
1828
  setLoading(false);
@@ -916,14 +1830,8 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
916
1830
  onReady({
917
1831
  getMjml: () => editor.getHtml(),
918
1832
  getCompiledHtml: compiledHtml,
919
- flushSave: async () => {
920
- if (saveTimer.current) clearTimeout(saveTimer.current);
921
- await save();
922
- },
923
- reloadFromDb: async () => {
924
- const fresh = await documents.get(docId);
925
- if (fresh.mjml !== editor.getHtml()) loadMjml(fresh.mjml);
926
- },
1833
+ flushSave: () => queue.flush(),
1834
+ reloadFromDb: reload,
927
1835
  highlightSection: (sectionId, on) => {
928
1836
  ensureHighlightStyles();
929
1837
  const el = sectionEl(sectionId);
@@ -939,10 +1847,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
939
1847
  notifyState();
940
1848
  },
941
1849
  undo: () => {
1850
+ if (readOnlyRef.current) return;
942
1851
  editor.UndoManager.undo();
943
1852
  notifyState();
944
1853
  },
945
1854
  redo: () => {
1855
+ if (readOnlyRef.current) return;
946
1856
  editor.UndoManager.redo();
947
1857
  notifyState();
948
1858
  },
@@ -951,6 +1861,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
951
1861
  notifyState();
952
1862
  },
953
1863
  setContentWidth: (w) => {
1864
+ if (readOnlyRef.current) return;
954
1865
  findByTag("mj-body")?.addAttributes({ width: w });
955
1866
  notifyState();
956
1867
  },
@@ -961,8 +1872,8 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
961
1872
  }
962
1873
  });
963
1874
  };
964
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
965
- import_react4.default,
1875
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1876
+ import_react6.default,
966
1877
  {
967
1878
  grapesjs: import_grapesjs.default,
968
1879
  className: "h-full",
@@ -975,6 +1886,19 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
975
1886
  styleManager: { appendTo: "#gjs-styles" },
976
1887
  traitManager: { appendTo: "#gjs-traits" },
977
1888
  layerManager: { appendTo: "#gjs-layers" },
1889
+ assetManager: {
1890
+ // Never inline files: mail clients block `data:` images (see ImageLibrary).
1891
+ embedAsBase64: false,
1892
+ // Our own dialog instead of GrapesJS's modal. GrapesJS still decides when it opens
1893
+ // and what a choice applies to; `select(url, true)` sets the image and closes.
1894
+ custom: {
1895
+ open: (props) => setPicker({
1896
+ current: props.options?.current ?? (String(props.options?.target?.get("src") ?? "") || void 0),
1897
+ select: (url) => props.select(url, true)
1898
+ }),
1899
+ close: () => setPicker(null)
1900
+ }
1901
+ },
978
1902
  deviceManager: {
979
1903
  devices: [
980
1904
  { id: "desktop", name: "Desktop", width: "" },
@@ -984,19 +1908,26 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
984
1908
  plugins: [import_grapesjs_mjml.default]
985
1909
  },
986
1910
  onEditor,
987
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex h-full min-h-0 flex-1", children: [
988
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1911
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
1912
+ readOnly && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
1913
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1914
+ " ",
1915
+ labels.agentWorking
1916
+ ] }) }),
1917
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
989
1918
  LeftSidebar,
990
1919
  {
991
1920
  view: sidebarView,
992
1921
  onViewChange: setSidebarView,
993
- markLayersSource: () => selectionSource.current = "layers"
1922
+ markLayersSource: () => selectionSource.current = "layers",
1923
+ selectedImageSrc,
1924
+ onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
994
1925
  }
995
1926
  ),
996
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
998
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react4.Canvas, { className: "h-full" }),
999
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1927
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
1928
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
1929
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react6.Canvas, { className: "h-full" }),
1930
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1000
1931
  CanvasComments,
1001
1932
  {
1002
1933
  docId,
@@ -1006,12 +1937,27 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1006
1937
  onOpenCountChange
1007
1938
  }
1008
1939
  ),
1009
- 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: [
1010
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Spinner, {}),
1940
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1941
+ ImagePicker,
1942
+ {
1943
+ open: picker !== null,
1944
+ currentUrl: picker?.current,
1945
+ onSelect: (url) => {
1946
+ if (!readOnlyRef.current) picker?.select(url);
1947
+ setPicker(null);
1948
+ },
1949
+ onClose: () => {
1950
+ setPicker(null);
1951
+ editorRef.current?.AssetManager.close();
1952
+ }
1953
+ }
1954
+ ),
1955
+ loading && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
1956
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1011
1957
  " Loading editor\u2026"
1012
1958
  ] })
1013
1959
  ] }),
1014
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1960
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1015
1961
  ] })
1016
1962
  ] })
1017
1963
  }
@@ -1022,32 +1968,34 @@ function CollapseSection({
1022
1968
  defaultOpen = true,
1023
1969
  children
1024
1970
  }) {
1025
- const [open, setOpen] = (0, import_react5.useState)(defaultOpen);
1026
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "border-b border-panel-border", children: [
1027
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1971
+ const [open, setOpen] = (0, import_react7.useState)(defaultOpen);
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border", children: [
1973
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1028
1974
  "button",
1029
1975
  {
1030
1976
  type: "button",
1031
1977
  onClick: () => setOpen((o) => !o),
1032
1978
  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",
1033
1979
  children: [
1034
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1980
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1035
1981
  title
1036
1982
  ]
1037
1983
  }
1038
1984
  ),
1039
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn(!open && "hidden"), children })
1985
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: cn(!open && "hidden"), children })
1040
1986
  ] });
1041
1987
  }
1042
1988
  function LeftSidebar({
1043
1989
  view,
1044
1990
  onViewChange,
1045
- markLayersSource
1991
+ markLayersSource,
1992
+ selectedImageSrc,
1993
+ onChooseImage
1046
1994
  }) {
1047
- const editor = (0, import_react4.useEditorMaybe)();
1995
+ const editor = (0, import_react6.useEditorMaybe)();
1048
1996
  const labels = useLabels();
1049
- const [layersExpanded, setLayersExpanded] = (0, import_react5.useState)(false);
1050
- const [blockSearch, setBlockSearch] = (0, import_react5.useState)("");
1997
+ const [layersExpanded, setLayersExpanded] = (0, import_react7.useState)(false);
1998
+ const [blockSearch, setBlockSearch] = (0, import_react7.useState)("");
1051
1999
  const changeView = (v) => {
1052
2000
  if (v === "settings" && editor && !editor.getSelected()) {
1053
2001
  const wrapper = editor.getWrapper();
@@ -1076,12 +2024,12 @@ function LeftSidebar({
1076
2024
  cat.style.display = anyVisible ? "" : "none";
1077
2025
  });
1078
2026
  };
1079
- 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: [
1080
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex border-b border-panel-border", children: [
2027
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
2028
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex border-b border-panel-border", children: [
1081
2029
  ["blocks", "Blocks"],
1082
2030
  ["settings", "Settings"],
1083
2031
  ["layers", "Layers"]
1084
- ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2032
+ ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1085
2033
  "button",
1086
2034
  {
1087
2035
  type: "button",
@@ -1095,10 +2043,10 @@ function LeftSidebar({
1095
2043
  },
1096
2044
  value
1097
2045
  )) }),
1098
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
1099
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
1100
- /* @__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" }),
1101
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2046
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
2047
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
2048
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react8.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
2049
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1102
2050
  Input,
1103
2051
  {
1104
2052
  value: blockSearch,
@@ -1107,23 +2055,58 @@ function LeftSidebar({
1107
2055
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1108
2056
  }
1109
2057
  ),
1110
- /* @__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" })
2058
+ /* @__PURE__ */ (0, import_jsx_runtime13.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" })
1111
2059
  ] }),
1112
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
2060
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1113
2061
  ] }),
1114
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1115
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-traits" }) }),
1116
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-styles" }) })
2062
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
2063
+ selectedImageSrc !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border p-3", children: [
2064
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2065
+ "button",
2066
+ {
2067
+ type: "button",
2068
+ onClick: onChooseImage,
2069
+ title: labels.chooseImage,
2070
+ className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
2071
+ children: [
2072
+ selectedImageSrc ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2073
+ "img",
2074
+ {
2075
+ src: selectedImageSrc,
2076
+ alt: "",
2077
+ className: "h-32 w-full bg-black/25 object-contain"
2078
+ }
2079
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageIcon, { className: "size-8" }) }),
2080
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-sm font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
2081
+ ]
2082
+ }
2083
+ ),
2084
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2085
+ Button,
2086
+ {
2087
+ type: "button",
2088
+ size: "sm",
2089
+ className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
2090
+ onClick: onChooseImage,
2091
+ children: [
2092
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageUp, {}),
2093
+ labels.chooseImage
2094
+ ]
2095
+ }
2096
+ )
2097
+ ] }),
2098
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-traits" }) }),
2099
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-styles" }) })
1117
2100
  ] }),
1118
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2101
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1119
2102
  "div",
1120
2103
  {
1121
2104
  className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
1122
2105
  onMouseDownCapture: markLayersSource,
1123
2106
  children: [
1124
- /* @__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: [
1125
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.expandAll }),
1126
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2107
+ /* @__PURE__ */ (0, import_jsx_runtime13.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: [
2108
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: labels.expandAll }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1127
2110
  Switch,
1128
2111
  {
1129
2112
  checked: layersExpanded,
@@ -1132,7 +2115,7 @@ function LeftSidebar({
1132
2115
  }
1133
2116
  )
1134
2117
  ] }),
1135
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
2118
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
1136
2119
  ]
1137
2120
  }
1138
2121
  )
@@ -1143,9 +2126,9 @@ function Breadcrumb({
1143
2126
  onSelect
1144
2127
  }) {
1145
2128
  const labels = useLabels();
1146
- 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: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center gap-0.5", children: [
1147
- i > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: "size-3 opacity-40" }),
1148
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2129
+ return /* @__PURE__ */ (0, import_jsx_runtime13.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_runtime13.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-0.5", children: [
2130
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: "size-3 opacity-40" }),
2131
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1149
2132
  "button",
1150
2133
  {
1151
2134
  type: "button",