@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/README.md +46 -0
- package/dist/canvas.cjs +1102 -119
- package/dist/canvas.cjs.map +1 -1
- package/dist/canvas.d.cts +2 -0
- package/dist/canvas.d.ts +2 -0
- package/dist/canvas.js +936 -83
- package/dist/canvas.js.map +1 -1
- package/dist/{chunk-D2Y633TP.js → chunk-SR3NM6VC.js} +130 -32
- package/dist/chunk-SR3NM6VC.js.map +1 -0
- package/dist/index.cjs +88 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +3 -1
- package/package.json +7 -4
- package/src/components/editor/editor-theme.css +29 -0
- package/dist/chunk-D2Y633TP.js.map +0 -1
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
|
|
40
|
+
var import_react6 = __toESM(require("@grapesjs/react"), 1);
|
|
41
41
|
var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
|
|
42
|
-
var
|
|
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/
|
|
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
|
|
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
|
|
287
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
462
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
482
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
486
|
-
] }) : /* @__PURE__ */ (0,
|
|
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,
|
|
635
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
|
|
490
636
|
] }) }),
|
|
491
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
644
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.X, { className: "size-4" })
|
|
499
645
|
}
|
|
500
646
|
)
|
|
501
647
|
] }),
|
|
502
|
-
/* @__PURE__ */ (0,
|
|
503
|
-
activeComments.length === 0 && /* @__PURE__ */ (0,
|
|
504
|
-
activeComments.map((c) => /* @__PURE__ */ (0,
|
|
505
|
-
/* @__PURE__ */ (0,
|
|
506
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
521
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
538
|
-
/* @__PURE__ */ (0,
|
|
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
|
|
552
|
-
var
|
|
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
|
|
556
|
-
var
|
|
557
|
-
var
|
|
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,
|
|
564
|
-
|
|
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,
|
|
574
|
-
|
|
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
|
|
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,
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
const
|
|
724
|
-
const [
|
|
725
|
-
const [
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const
|
|
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", () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
editor.getWrapper()?.find(
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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:
|
|
920
|
-
|
|
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,
|
|
965
|
-
|
|
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,
|
|
988
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
997
|
-
/* @__PURE__ */ (0,
|
|
998
|
-
/* @__PURE__ */ (0,
|
|
999
|
-
/* @__PURE__ */ (0,
|
|
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
|
-
|
|
1010
|
-
|
|
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,
|
|
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,
|
|
1026
|
-
return /* @__PURE__ */ (0,
|
|
1027
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
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,
|
|
1995
|
+
const editor = (0, import_react6.useEditorMaybe)();
|
|
1048
1996
|
const labels = useLabels();
|
|
1049
|
-
const [layersExpanded, setLayersExpanded] = (0,
|
|
1050
|
-
const [blockSearch, setBlockSearch] = (0,
|
|
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,
|
|
1080
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
1099
|
-
/* @__PURE__ */ (0,
|
|
1100
|
-
/* @__PURE__ */ (0,
|
|
1101
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
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,
|
|
1115
|
-
|
|
1116
|
-
|
|
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,
|
|
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,
|
|
1125
|
-
/* @__PURE__ */ (0,
|
|
1126
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
1147
|
-
i > 0 && /* @__PURE__ */ (0,
|
|
1148
|
-
/* @__PURE__ */ (0,
|
|
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",
|