@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.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
CanvasComments,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
Popover,
|
|
10
|
+
PopoverAnchor,
|
|
11
|
+
PopoverContent,
|
|
12
|
+
PopoverTrigger,
|
|
13
|
+
refusal,
|
|
14
|
+
saveToastId,
|
|
15
|
+
useAgentTurn,
|
|
4
16
|
useDocumentStore,
|
|
17
|
+
useImageLibrary,
|
|
5
18
|
useLabels
|
|
6
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-SR3NM6VC.js";
|
|
7
20
|
import {
|
|
21
|
+
Button,
|
|
8
22
|
Input,
|
|
9
23
|
Spinner,
|
|
10
24
|
cn
|
|
@@ -14,10 +28,41 @@ import {
|
|
|
14
28
|
import grapesjs from "grapesjs";
|
|
15
29
|
import GjsEditor, { Canvas, useEditorMaybe } from "@grapesjs/react";
|
|
16
30
|
import grapesjsMJML from "grapesjs-mjml";
|
|
17
|
-
import { useRef, useState } from "react";
|
|
31
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
18
32
|
import { STARTER_MJML } from "@mjml-agent-editor/core";
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
|
|
34
|
+
// src/lib/save-queue.ts
|
|
35
|
+
function createSaveQueue(save, delayMs) {
|
|
36
|
+
let timer = null;
|
|
37
|
+
let tail = Promise.resolve();
|
|
38
|
+
const cancel = () => {
|
|
39
|
+
if (timer) clearTimeout(timer);
|
|
40
|
+
timer = null;
|
|
41
|
+
};
|
|
42
|
+
const run = (task) => {
|
|
43
|
+
const next = tail.then(task);
|
|
44
|
+
tail = next.catch(() => {
|
|
45
|
+
});
|
|
46
|
+
return next;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
schedule() {
|
|
50
|
+
cancel();
|
|
51
|
+
timer = setTimeout(() => void run(save).catch(() => {
|
|
52
|
+
}), delayMs);
|
|
53
|
+
},
|
|
54
|
+
cancel,
|
|
55
|
+
flush() {
|
|
56
|
+
cancel();
|
|
57
|
+
return run(save);
|
|
58
|
+
},
|
|
59
|
+
run
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/components/editor/EmailEditor.tsx
|
|
64
|
+
import { toast as toast2 } from "sonner";
|
|
65
|
+
import { ChevronRight, ImageIcon as ImageIcon2, ImageUp as ImageUp2 } from "lucide-react";
|
|
21
66
|
|
|
22
67
|
// src/components/ui/switch.tsx
|
|
23
68
|
import "react";
|
|
@@ -51,7 +96,582 @@ function Switch({
|
|
|
51
96
|
|
|
52
97
|
// src/components/editor/EmailEditor.tsx
|
|
53
98
|
import { Search } from "lucide-react";
|
|
99
|
+
|
|
100
|
+
// src/components/editor/ImagePicker.tsx
|
|
101
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
102
|
+
import { Check, ImageUp, Link2, Trash2 } from "lucide-react";
|
|
103
|
+
import { toast } from "sonner";
|
|
54
104
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
105
|
+
function ImagePicker({
|
|
106
|
+
open,
|
|
107
|
+
currentUrl,
|
|
108
|
+
onSelect,
|
|
109
|
+
onClose
|
|
110
|
+
}) {
|
|
111
|
+
const library = useImageLibrary();
|
|
112
|
+
const labels = useLabels();
|
|
113
|
+
const [images, setImages] = useState([]);
|
|
114
|
+
const [quota, setQuota] = useState();
|
|
115
|
+
const [loading, setLoading] = useState(false);
|
|
116
|
+
const [pending, setPending] = useState(0);
|
|
117
|
+
const [dragging, setDragging] = useState(false);
|
|
118
|
+
const [url, setUrl] = useState("");
|
|
119
|
+
const [confirmId, setConfirmId] = useState(null);
|
|
120
|
+
const fileInput = useRef(null);
|
|
121
|
+
const session = useRef(0);
|
|
122
|
+
const latest = useRef({ labels, onSelect });
|
|
123
|
+
latest.current = { labels, onSelect };
|
|
124
|
+
const lists = useRef(0);
|
|
125
|
+
const refresh = useCallback(async () => {
|
|
126
|
+
if (!library) return;
|
|
127
|
+
const mine = ++lists.current;
|
|
128
|
+
setLoading(true);
|
|
129
|
+
try {
|
|
130
|
+
const result = await library.list();
|
|
131
|
+
if (mine !== lists.current) return;
|
|
132
|
+
setImages(result.images);
|
|
133
|
+
setQuota(result.quota);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(err);
|
|
136
|
+
if (mine === lists.current) toast.error(latest.current.labels.imagePickerLoadFailed);
|
|
137
|
+
} finally {
|
|
138
|
+
if (mine === lists.current) setLoading(false);
|
|
139
|
+
}
|
|
140
|
+
}, [library]);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
session.current += 1;
|
|
143
|
+
if (!open) return;
|
|
144
|
+
setUrl("");
|
|
145
|
+
setConfirmId(null);
|
|
146
|
+
void refresh();
|
|
147
|
+
}, [open]);
|
|
148
|
+
useEffect(() => () => void (session.current += 1), []);
|
|
149
|
+
const accepts = (file) => matchesAccept(file, library?.accept);
|
|
150
|
+
const full = quota !== void 0 && quota.used >= quota.limit;
|
|
151
|
+
const upload = async (files) => {
|
|
152
|
+
if (!library || !files?.length) return;
|
|
153
|
+
const mine = session.current;
|
|
154
|
+
const list = Array.from(files).filter((f) => {
|
|
155
|
+
if (accepts(f)) return true;
|
|
156
|
+
toast.error(labels.imagePickerUnsupported(f.name));
|
|
157
|
+
return false;
|
|
158
|
+
});
|
|
159
|
+
if (!list.length) return;
|
|
160
|
+
setPending((n) => n + list.length);
|
|
161
|
+
const results = await Promise.allSettled(list.map(async (f) => library.upload(f)));
|
|
162
|
+
setPending((n) => n - list.length);
|
|
163
|
+
for (const r of results) {
|
|
164
|
+
if (r.status === "rejected") {
|
|
165
|
+
console.error(r.reason);
|
|
166
|
+
toast.error(r.reason instanceof Error ? r.reason.message : labels.imageUploadFailed);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (session.current !== mine) return;
|
|
170
|
+
await refresh();
|
|
171
|
+
const [only] = results;
|
|
172
|
+
if (results.length === 1 && only?.status === "fulfilled" && session.current === mine) {
|
|
173
|
+
latest.current.onSelect(only.value.url);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const remove = async (image) => {
|
|
177
|
+
if (!library?.remove) return;
|
|
178
|
+
if (confirmId !== image.id) {
|
|
179
|
+
setConfirmId(image.id);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setConfirmId(null);
|
|
183
|
+
try {
|
|
184
|
+
await library.remove(image.id);
|
|
185
|
+
await refresh();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(err);
|
|
188
|
+
toast.error(err instanceof Error ? err.message : labels.imageDeleteFailed);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const onDrop = (e) => {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
setDragging(false);
|
|
194
|
+
if (!full) void upload(e.dataTransfer.files);
|
|
195
|
+
};
|
|
196
|
+
return /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ 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: [
|
|
197
|
+
/* @__PURE__ */ jsxs(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
|
|
198
|
+
/* @__PURE__ */ jsx2(DialogTitle, { children: labels.imagePickerTitle }),
|
|
199
|
+
quota && /* @__PURE__ */ jsx2(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
|
|
200
|
+
] }),
|
|
201
|
+
/* @__PURE__ */ jsxs("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
|
|
202
|
+
library && (full ? /* @__PURE__ */ jsx2("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__ */ jsxs(
|
|
203
|
+
"div",
|
|
204
|
+
{
|
|
205
|
+
onDragOver: (e) => {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
setDragging(true);
|
|
208
|
+
},
|
|
209
|
+
onDragLeave: () => setDragging(false),
|
|
210
|
+
onDrop,
|
|
211
|
+
className: cn(
|
|
212
|
+
"flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
213
|
+
dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
|
|
214
|
+
),
|
|
215
|
+
children: [
|
|
216
|
+
/* @__PURE__ */ jsx2(ImageUp, { className: "size-8 text-panel-muted-fg" }),
|
|
217
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
|
|
218
|
+
/* @__PURE__ */ jsx2("span", { children: labels.imagePickerDrop }),
|
|
219
|
+
/* @__PURE__ */ jsx2(
|
|
220
|
+
Button,
|
|
221
|
+
{
|
|
222
|
+
type: "button",
|
|
223
|
+
size: "sm",
|
|
224
|
+
className: "bg-brand text-brand-fg hover:bg-brand/90",
|
|
225
|
+
onClick: () => fileInput.current?.click(),
|
|
226
|
+
children: labels.imagePickerBrowse
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ jsx2(
|
|
231
|
+
"input",
|
|
232
|
+
{
|
|
233
|
+
ref: fileInput,
|
|
234
|
+
type: "file",
|
|
235
|
+
accept: library.accept ?? "image/*",
|
|
236
|
+
multiple: true,
|
|
237
|
+
hidden: true,
|
|
238
|
+
onChange: (e) => {
|
|
239
|
+
void upload(e.target.files);
|
|
240
|
+
e.target.value = "";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
)),
|
|
247
|
+
/* @__PURE__ */ jsxs(
|
|
248
|
+
"form",
|
|
249
|
+
{
|
|
250
|
+
className: "flex gap-2",
|
|
251
|
+
onSubmit: (e) => {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
const value = url.trim();
|
|
254
|
+
if (value) onSelect(value);
|
|
255
|
+
},
|
|
256
|
+
children: [
|
|
257
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
258
|
+
/* @__PURE__ */ jsx2(Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
|
|
259
|
+
/* @__PURE__ */ jsx2(
|
|
260
|
+
Input,
|
|
261
|
+
{
|
|
262
|
+
value: url,
|
|
263
|
+
onChange: (e) => setUrl(e.target.value),
|
|
264
|
+
placeholder: labels.imagePickerUrlPlaceholder,
|
|
265
|
+
className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
] }),
|
|
269
|
+
/* @__PURE__ */ jsx2(
|
|
270
|
+
Button,
|
|
271
|
+
{
|
|
272
|
+
type: "submit",
|
|
273
|
+
size: "lg",
|
|
274
|
+
variant: "outline",
|
|
275
|
+
disabled: !url.trim(),
|
|
276
|
+
className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
|
|
277
|
+
children: labels.imagePickerUseUrl
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
),
|
|
283
|
+
library && /* @__PURE__ */ jsxs("section", { children: [
|
|
284
|
+
/* @__PURE__ */ jsx2("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
|
|
285
|
+
loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ jsx2(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ jsx2("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ jsxs("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
|
|
286
|
+
Array.from({ length: pending }, (_, i) => /* @__PURE__ */ jsx2(
|
|
287
|
+
"li",
|
|
288
|
+
{
|
|
289
|
+
className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
|
|
290
|
+
children: /* @__PURE__ */ jsx2(Spinner, {})
|
|
291
|
+
},
|
|
292
|
+
`pending-${i}`
|
|
293
|
+
)),
|
|
294
|
+
images.map((image) => {
|
|
295
|
+
const selected = image.url === currentUrl;
|
|
296
|
+
const confirming = confirmId === image.id;
|
|
297
|
+
return /* @__PURE__ */ jsxs("li", { className: "group relative", children: [
|
|
298
|
+
/* @__PURE__ */ jsx2(
|
|
299
|
+
"button",
|
|
300
|
+
{
|
|
301
|
+
type: "button",
|
|
302
|
+
onClick: () => onSelect(image.url),
|
|
303
|
+
title: image.name,
|
|
304
|
+
className: cn(
|
|
305
|
+
"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",
|
|
306
|
+
selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
|
|
307
|
+
),
|
|
308
|
+
children: /* @__PURE__ */ jsx2(
|
|
309
|
+
"img",
|
|
310
|
+
{
|
|
311
|
+
src: image.url,
|
|
312
|
+
alt: image.name ?? "",
|
|
313
|
+
loading: "lazy",
|
|
314
|
+
className: "size-full object-contain"
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
),
|
|
319
|
+
selected && /* @__PURE__ */ jsx2("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__ */ jsx2(Check, { className: "size-3" }) }),
|
|
320
|
+
image.name && /* @__PURE__ */ jsx2("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 }),
|
|
321
|
+
library.remove && /* @__PURE__ */ jsxs(
|
|
322
|
+
"button",
|
|
323
|
+
{
|
|
324
|
+
type: "button",
|
|
325
|
+
onClick: () => void remove(image),
|
|
326
|
+
onBlur: () => confirming && setConfirmId(null),
|
|
327
|
+
title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
|
|
328
|
+
"aria-label": labels.imagePickerDelete,
|
|
329
|
+
className: cn(
|
|
330
|
+
"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",
|
|
331
|
+
confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
|
|
332
|
+
),
|
|
333
|
+
children: [
|
|
334
|
+
/* @__PURE__ */ jsx2(Trash2, { className: "size-3.5" }),
|
|
335
|
+
confirming && labels.imagePickerConfirmDelete
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
] }, image.id);
|
|
340
|
+
})
|
|
341
|
+
] })
|
|
342
|
+
] })
|
|
343
|
+
] })
|
|
344
|
+
] }) });
|
|
345
|
+
}
|
|
346
|
+
function matchesAccept(file, accept) {
|
|
347
|
+
if (!accept) return file.type.startsWith("image/");
|
|
348
|
+
return accept.split(",").some((raw) => {
|
|
349
|
+
const rule = raw.trim().toLowerCase();
|
|
350
|
+
if (rule.startsWith(".")) return file.name.toLowerCase().endsWith(rule);
|
|
351
|
+
if (rule.endsWith("/*")) return file.type.startsWith(rule.slice(0, -1));
|
|
352
|
+
return file.type === rule;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/components/editor/ColorField.tsx
|
|
357
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
358
|
+
import { HexAlphaColorPicker, HexColorPicker } from "react-colorful";
|
|
359
|
+
import { Pipette } from "lucide-react";
|
|
360
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
361
|
+
var PRESETS = [
|
|
362
|
+
"#000000",
|
|
363
|
+
"#374151",
|
|
364
|
+
"#6b7280",
|
|
365
|
+
"#d1d5db",
|
|
366
|
+
"#f3f4f6",
|
|
367
|
+
"#ffffff",
|
|
368
|
+
"#dc2626",
|
|
369
|
+
"#ea580c",
|
|
370
|
+
"#ca8a04",
|
|
371
|
+
"#16a34a",
|
|
372
|
+
"#0284c7",
|
|
373
|
+
"#4f46e5",
|
|
374
|
+
"#9333ea",
|
|
375
|
+
"#db2777"
|
|
376
|
+
];
|
|
377
|
+
var eyeDropper = () => globalThis.EyeDropper;
|
|
378
|
+
function ColorField({
|
|
379
|
+
value,
|
|
380
|
+
placeholder,
|
|
381
|
+
documentColors,
|
|
382
|
+
labels,
|
|
383
|
+
onChange
|
|
384
|
+
}) {
|
|
385
|
+
const [text, setText] = useState2(value);
|
|
386
|
+
const [open, setOpen] = useState2(false);
|
|
387
|
+
const [used, setUsed] = useState2([]);
|
|
388
|
+
const pressed = useRef2(false);
|
|
389
|
+
const dragged = useRef2(null);
|
|
390
|
+
const fieldRef = useRef2(null);
|
|
391
|
+
const anchor = useRef2({
|
|
392
|
+
getBoundingClientRect: () => {
|
|
393
|
+
const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
|
|
394
|
+
const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
|
|
395
|
+
return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
useEffect2(() => setText(value), [value]);
|
|
399
|
+
const latestOnChange = useRef2(onChange);
|
|
400
|
+
latestOnChange.current = onChange;
|
|
401
|
+
const release = useCallback2(() => {
|
|
402
|
+
pressed.current = false;
|
|
403
|
+
if (dragged.current !== null) latestOnChange.current(dragged.current, false);
|
|
404
|
+
dragged.current = null;
|
|
405
|
+
}, []);
|
|
406
|
+
useEffect2(() => {
|
|
407
|
+
document.addEventListener("lostpointercapture", release);
|
|
408
|
+
return () => {
|
|
409
|
+
document.removeEventListener("lostpointercapture", release);
|
|
410
|
+
release();
|
|
411
|
+
};
|
|
412
|
+
}, [release]);
|
|
413
|
+
const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
|
|
414
|
+
const translucent = hex.length === 9;
|
|
415
|
+
const empty = !value;
|
|
416
|
+
const commit = (next) => {
|
|
417
|
+
dragged.current = null;
|
|
418
|
+
onChange(next, false);
|
|
419
|
+
};
|
|
420
|
+
const drag = (next) => {
|
|
421
|
+
setText(next);
|
|
422
|
+
if (!pressed.current) return commit(next);
|
|
423
|
+
dragged.current = next;
|
|
424
|
+
onChange(next, true);
|
|
425
|
+
};
|
|
426
|
+
const pickFromScreen = async () => {
|
|
427
|
+
const Ctor = eyeDropper();
|
|
428
|
+
if (!Ctor) return;
|
|
429
|
+
try {
|
|
430
|
+
const { sRGBHex } = await new Ctor().open();
|
|
431
|
+
commit(sRGBHex);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
return /* @__PURE__ */ jsxs2(
|
|
436
|
+
Popover,
|
|
437
|
+
{
|
|
438
|
+
open,
|
|
439
|
+
onOpenChange: (next) => {
|
|
440
|
+
if (next) setUsed(documentColors());
|
|
441
|
+
else if (text.trim() !== value) commit(text.trim());
|
|
442
|
+
setOpen(next);
|
|
443
|
+
},
|
|
444
|
+
children: [
|
|
445
|
+
/* @__PURE__ */ jsx3(PopoverAnchor, { virtualRef: anchor }),
|
|
446
|
+
/* @__PURE__ */ jsxs2(
|
|
447
|
+
"div",
|
|
448
|
+
{
|
|
449
|
+
ref: fieldRef,
|
|
450
|
+
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",
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ jsx3(
|
|
453
|
+
"input",
|
|
454
|
+
{
|
|
455
|
+
value: text,
|
|
456
|
+
placeholder,
|
|
457
|
+
spellCheck: false,
|
|
458
|
+
onChange: (e) => setText(e.target.value),
|
|
459
|
+
onBlur: () => text !== value && commit(text.trim()),
|
|
460
|
+
onKeyDown: (e) => {
|
|
461
|
+
if (e.key === "Enter") commit(text.trim());
|
|
462
|
+
if (e.key === "Escape") setText(value);
|
|
463
|
+
},
|
|
464
|
+
className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
|
|
465
|
+
}
|
|
466
|
+
),
|
|
467
|
+
/* @__PURE__ */ jsx3(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
|
|
468
|
+
"button",
|
|
469
|
+
{
|
|
470
|
+
type: "button",
|
|
471
|
+
title: labels.colorPickerOpen,
|
|
472
|
+
"aria-label": labels.colorPickerOpen,
|
|
473
|
+
className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
|
|
474
|
+
children: [
|
|
475
|
+
/* @__PURE__ */ jsx3(Checkerboard, {}),
|
|
476
|
+
!empty && /* @__PURE__ */ jsx3("span", { className: "absolute inset-0", style: { background: value } })
|
|
477
|
+
]
|
|
478
|
+
}
|
|
479
|
+
) })
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
),
|
|
483
|
+
/* @__PURE__ */ jsxs2(
|
|
484
|
+
PopoverContent,
|
|
485
|
+
{
|
|
486
|
+
side: "right",
|
|
487
|
+
align: "start",
|
|
488
|
+
sideOffset: 8,
|
|
489
|
+
className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
|
|
490
|
+
children: [
|
|
491
|
+
/* @__PURE__ */ jsx3(
|
|
492
|
+
"div",
|
|
493
|
+
{
|
|
494
|
+
onPointerDownCapture: (e) => {
|
|
495
|
+
pressed.current = true;
|
|
496
|
+
e.target.setPointerCapture(e.pointerId);
|
|
497
|
+
},
|
|
498
|
+
children: translucent ? /* @__PURE__ */ jsx3(HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ jsx3(HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
|
|
502
|
+
/* @__PURE__ */ jsx3(
|
|
503
|
+
"span",
|
|
504
|
+
{
|
|
505
|
+
className: "size-6 shrink-0 rounded border border-white/20",
|
|
506
|
+
style: { background: hex }
|
|
507
|
+
}
|
|
508
|
+
),
|
|
509
|
+
/* @__PURE__ */ jsx3(
|
|
510
|
+
"input",
|
|
511
|
+
{
|
|
512
|
+
value: text,
|
|
513
|
+
spellCheck: false,
|
|
514
|
+
onChange: (e) => setText(e.target.value),
|
|
515
|
+
onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
|
|
516
|
+
onBlur: () => text !== value && commit(text.trim()),
|
|
517
|
+
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"
|
|
518
|
+
}
|
|
519
|
+
),
|
|
520
|
+
eyeDropper() && /* @__PURE__ */ jsx3(
|
|
521
|
+
"button",
|
|
522
|
+
{
|
|
523
|
+
type: "button",
|
|
524
|
+
onClick: () => void pickFromScreen(),
|
|
525
|
+
title: labels.colorPickerEyedropper,
|
|
526
|
+
"aria-label": labels.colorPickerEyedropper,
|
|
527
|
+
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",
|
|
528
|
+
children: /* @__PURE__ */ jsx3(Pipette, { className: "size-3" })
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
] }),
|
|
532
|
+
used.length > 0 && /* @__PURE__ */ jsx3(
|
|
533
|
+
Swatches,
|
|
534
|
+
{
|
|
535
|
+
title: labels.colorPickerDocument,
|
|
536
|
+
colors: used,
|
|
537
|
+
current: hex,
|
|
538
|
+
onPick: commit
|
|
539
|
+
}
|
|
540
|
+
),
|
|
541
|
+
/* @__PURE__ */ jsx3(
|
|
542
|
+
Swatches,
|
|
543
|
+
{
|
|
544
|
+
title: labels.colorPickerPresets,
|
|
545
|
+
colors: PRESETS,
|
|
546
|
+
current: hex,
|
|
547
|
+
onPick: commit,
|
|
548
|
+
transparentLabel: labels.colorPickerTransparent
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
]
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
function Swatches({
|
|
559
|
+
title,
|
|
560
|
+
colors,
|
|
561
|
+
current,
|
|
562
|
+
onPick,
|
|
563
|
+
transparentLabel
|
|
564
|
+
}) {
|
|
565
|
+
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
566
|
+
/* @__PURE__ */ jsx3("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
|
|
567
|
+
/* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-8 gap-1", children: [
|
|
568
|
+
colors.map((color) => /* @__PURE__ */ jsx3(
|
|
569
|
+
"button",
|
|
570
|
+
{
|
|
571
|
+
type: "button",
|
|
572
|
+
title: color,
|
|
573
|
+
onClick: () => onPick(color),
|
|
574
|
+
className: cn(
|
|
575
|
+
"aspect-square rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
|
|
576
|
+
color.toLowerCase() === current.toLowerCase() && "ring-2 ring-brand"
|
|
577
|
+
),
|
|
578
|
+
style: { background: color }
|
|
579
|
+
},
|
|
580
|
+
color
|
|
581
|
+
)),
|
|
582
|
+
transparentLabel && /* @__PURE__ */ jsx3(
|
|
583
|
+
"button",
|
|
584
|
+
{
|
|
585
|
+
type: "button",
|
|
586
|
+
title: transparentLabel,
|
|
587
|
+
onClick: () => onPick("transparent"),
|
|
588
|
+
className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
|
|
589
|
+
children: /* @__PURE__ */ jsx3(Checkerboard, {})
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
] })
|
|
593
|
+
] });
|
|
594
|
+
}
|
|
595
|
+
function Checkerboard() {
|
|
596
|
+
return /* @__PURE__ */ jsx3(
|
|
597
|
+
"span",
|
|
598
|
+
{
|
|
599
|
+
className: "absolute inset-0",
|
|
600
|
+
style: {
|
|
601
|
+
backgroundImage: "repeating-conic-gradient(#9ca3af 0% 25%, #e5e7eb 0% 50%)",
|
|
602
|
+
backgroundSize: "8px 8px"
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
function toHex(value, { opaque = false } = {}) {
|
|
608
|
+
const v = value.trim().toLowerCase();
|
|
609
|
+
const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
|
|
610
|
+
if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
|
|
611
|
+
if (/^#[0-9a-f]{6}$/.test(v)) return v;
|
|
612
|
+
if (/^#[0-9a-f]{8}$/.test(v)) return opaque || v.endsWith("ff") ? v.slice(0, 7) : v;
|
|
613
|
+
const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?/.exec(v);
|
|
614
|
+
if (rgb) {
|
|
615
|
+
const channels = [rgb[1], rgb[2], rgb[3]].map((n) => Math.min(255, Number(n)));
|
|
616
|
+
const alphaText = rgb[4];
|
|
617
|
+
const alpha = alphaText?.endsWith("%") ? parseFloat(alphaText) / 100 : Number(alphaText ?? 1);
|
|
618
|
+
if (!opaque && alpha < 1) channels.push(Math.round(Math.max(0, alpha) * 255));
|
|
619
|
+
return `#${channels.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/components/editor/ImageField.tsx
|
|
625
|
+
import { ImageIcon, X } from "lucide-react";
|
|
626
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
627
|
+
function ImageField({
|
|
628
|
+
value,
|
|
629
|
+
labels,
|
|
630
|
+
onChoose,
|
|
631
|
+
onClear
|
|
632
|
+
}) {
|
|
633
|
+
const url = imageUrl(value);
|
|
634
|
+
return /* @__PURE__ */ jsxs3("div", { className: "relative", children: [
|
|
635
|
+
/* @__PURE__ */ jsxs3(
|
|
636
|
+
"button",
|
|
637
|
+
{
|
|
638
|
+
type: "button",
|
|
639
|
+
onClick: onChoose,
|
|
640
|
+
title: labels.chooseImage,
|
|
641
|
+
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",
|
|
642
|
+
children: [
|
|
643
|
+
url ? /* @__PURE__ */ jsx4("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ jsxs3("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
|
|
644
|
+
/* @__PURE__ */ jsx4(ImageIcon, { className: "size-5" }),
|
|
645
|
+
labels.chooseImage
|
|
646
|
+
] }),
|
|
647
|
+
url && /* @__PURE__ */ jsx4("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 })
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
value && /* @__PURE__ */ jsx4(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
type: "button",
|
|
655
|
+
onClick: onClear,
|
|
656
|
+
title: labels.imageFieldRemove,
|
|
657
|
+
"aria-label": labels.imageFieldRemove,
|
|
658
|
+
className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
|
|
659
|
+
children: /* @__PURE__ */ jsx4(X, { className: "size-3" })
|
|
660
|
+
}
|
|
661
|
+
)
|
|
662
|
+
] });
|
|
663
|
+
}
|
|
664
|
+
function imageUrl(value) {
|
|
665
|
+
const v = value.trim();
|
|
666
|
+
if (!v || v === "none") return null;
|
|
667
|
+
const css = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(v);
|
|
668
|
+
if (css) return css[2] || null;
|
|
669
|
+
return /^[a-z-]+\(/i.test(v) ? null : v;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/components/editor/EmailEditor.tsx
|
|
673
|
+
import { createRoot } from "react-dom/client";
|
|
674
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
55
675
|
var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
|
|
56
676
|
var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
|
|
57
677
|
var TYPE_LABEL = {
|
|
@@ -64,6 +684,7 @@ function tagOf(c) {
|
|
|
64
684
|
return String(c.get("tagName") || c.get("type") || "");
|
|
65
685
|
}
|
|
66
686
|
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>';
|
|
687
|
+
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>';
|
|
67
688
|
function isSection(c) {
|
|
68
689
|
return tagOf(c) === "mj-section";
|
|
69
690
|
}
|
|
@@ -146,6 +767,75 @@ function wrapSelectionStyle(el, style) {
|
|
|
146
767
|
} catch {
|
|
147
768
|
}
|
|
148
769
|
}
|
|
770
|
+
function setupStyleFields(editor, labels) {
|
|
771
|
+
const sm = editor.StyleManager;
|
|
772
|
+
const base = sm.getType("base").view;
|
|
773
|
+
const documentColors = () => {
|
|
774
|
+
const source = `${editor.getHtml()} ${editor.getCss() ?? ""}`;
|
|
775
|
+
const counts = /* @__PURE__ */ new Map();
|
|
776
|
+
const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
|
|
777
|
+
for (const match of source.matchAll(colorValue)) {
|
|
778
|
+
const hex = toHex(match[1], { opaque: true });
|
|
779
|
+
if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
|
|
780
|
+
}
|
|
781
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
|
|
782
|
+
};
|
|
783
|
+
const ownValue = (property) => property.hasValue?.({ noParent: true }) ? String(property.getValue?.() ?? "") : "";
|
|
784
|
+
const reactView = (field) => base.extend({
|
|
785
|
+
create({ property }) {
|
|
786
|
+
const el = document.createElement("div");
|
|
787
|
+
el.addEventListener("change", (event) => event.stopPropagation());
|
|
788
|
+
const root = createRoot(el);
|
|
789
|
+
this.__root = root;
|
|
790
|
+
this.__render = () => root.render(field(property, ownValue(property)));
|
|
791
|
+
this.__render();
|
|
792
|
+
return el;
|
|
793
|
+
},
|
|
794
|
+
update() {
|
|
795
|
+
this.__render?.();
|
|
796
|
+
},
|
|
797
|
+
destroy() {
|
|
798
|
+
const root = this.__root;
|
|
799
|
+
this.__root = void 0;
|
|
800
|
+
this.__render = void 0;
|
|
801
|
+
setTimeout(() => root?.unmount());
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
sm.addType("color", {
|
|
805
|
+
view: reactView((property, value) => /* @__PURE__ */ jsx5(
|
|
806
|
+
ColorField,
|
|
807
|
+
{
|
|
808
|
+
value,
|
|
809
|
+
placeholder: String(property.getDefaultValue?.() ?? ""),
|
|
810
|
+
documentColors,
|
|
811
|
+
labels,
|
|
812
|
+
onChange: (next, partial) => property.upValue(next, { partial })
|
|
813
|
+
}
|
|
814
|
+
))
|
|
815
|
+
});
|
|
816
|
+
sm.addType("file", {
|
|
817
|
+
view: reactView((property, value) => /* @__PURE__ */ jsx5(
|
|
818
|
+
ImageField,
|
|
819
|
+
{
|
|
820
|
+
value,
|
|
821
|
+
labels,
|
|
822
|
+
onClear: () => property.upValue(""),
|
|
823
|
+
onChoose: () => {
|
|
824
|
+
const am = editor.AssetManager;
|
|
825
|
+
am.open({
|
|
826
|
+
types: ["image"],
|
|
827
|
+
current: imageUrl(value) ?? void 0,
|
|
828
|
+
select(asset, complete) {
|
|
829
|
+
const url = typeof asset === "string" ? asset : asset.getSrc();
|
|
830
|
+
property.upValue(url, { partial: !complete });
|
|
831
|
+
if (complete) am.close();
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
))
|
|
837
|
+
});
|
|
838
|
+
}
|
|
149
839
|
function setupRichText(editor, labels) {
|
|
150
840
|
const rte = editor.RichTextEditor;
|
|
151
841
|
if (!rte || rte.__customActions) return;
|
|
@@ -185,16 +875,25 @@ function setupRichText(editor, labels) {
|
|
|
185
875
|
function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
186
876
|
const documents = useDocumentStore();
|
|
187
877
|
const labels = useLabels();
|
|
188
|
-
const loadingRef =
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const [
|
|
193
|
-
const [
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
878
|
+
const loadingRef = useRef3(false);
|
|
879
|
+
const saveStatusRef = useRef3("idle");
|
|
880
|
+
const stateListeners = useRef3(/* @__PURE__ */ new Set());
|
|
881
|
+
const [loading, setLoading] = useState3(true);
|
|
882
|
+
const [sidebarView, setSidebarView] = useState3("blocks");
|
|
883
|
+
const [composeTarget, setComposeTarget] = useState3(null);
|
|
884
|
+
const selectionSource = useRef3("other");
|
|
885
|
+
const editorRef = useRef3(null);
|
|
886
|
+
const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
|
|
887
|
+
const readOnlyRef = useRef3(false);
|
|
888
|
+
const applyReadOnly = useRef3(async () => {
|
|
889
|
+
});
|
|
890
|
+
useEffect3(() => void applyReadOnly.current(turnRunning), [turnRunning]);
|
|
891
|
+
useEffect3(() => () => setLocked(false), [setLocked]);
|
|
892
|
+
const [crumbs, setCrumbs] = useState3([]);
|
|
893
|
+
const [selectedImageSrc, setSelectedImageSrc] = useState3(null);
|
|
894
|
+
const [picker, setPicker] = useState3(
|
|
895
|
+
null
|
|
896
|
+
);
|
|
198
897
|
const onEditor = async (editor) => {
|
|
199
898
|
editorRef.current = editor;
|
|
200
899
|
const buildCrumbs = (c) => {
|
|
@@ -221,11 +920,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
221
920
|
};
|
|
222
921
|
const snapshot = () => ({
|
|
223
922
|
device: editor.getDevice(),
|
|
224
|
-
canUndo: editor.UndoManager.hasUndo(),
|
|
225
|
-
canRedo: editor.UndoManager.hasRedo(),
|
|
923
|
+
canUndo: !readOnlyRef.current && editor.UndoManager.hasUndo(),
|
|
924
|
+
canRedo: !readOnlyRef.current && editor.UndoManager.hasRedo(),
|
|
226
925
|
saveStatus: saveStatusRef.current,
|
|
227
926
|
zoom: Math.round(editor.Canvas.getZoom?.() ?? 100),
|
|
228
|
-
contentWidth: bodyWidth()
|
|
927
|
+
contentWidth: bodyWidth(),
|
|
928
|
+
readOnly: readOnlyRef.current
|
|
229
929
|
});
|
|
230
930
|
const notifyState = () => {
|
|
231
931
|
const s = snapshot();
|
|
@@ -249,6 +949,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
249
949
|
if (isSection(c)) addIdClass(c, "sec");
|
|
250
950
|
if (!isCommentable(c)) return;
|
|
251
951
|
const toolbar = [...c.get("toolbar") ?? []];
|
|
952
|
+
if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
|
|
953
|
+
toolbar.push({
|
|
954
|
+
command: "open-image-picker",
|
|
955
|
+
label: IMAGE_SVG,
|
|
956
|
+
attributes: { title: labels.chooseImage }
|
|
957
|
+
});
|
|
958
|
+
c.set("toolbar", toolbar);
|
|
959
|
+
}
|
|
252
960
|
if (!toolbar.some((t) => t.command === "open-comments")) {
|
|
253
961
|
toolbar.push({
|
|
254
962
|
command: "open-comments",
|
|
@@ -258,6 +966,21 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
258
966
|
c.set("toolbar", toolbar);
|
|
259
967
|
}
|
|
260
968
|
};
|
|
969
|
+
editor.Commands.add("open-image-picker", {
|
|
970
|
+
run(ed) {
|
|
971
|
+
const target = ed.getSelected();
|
|
972
|
+
if (!target || tagOf(target) !== "mj-image") return;
|
|
973
|
+
const am = ed.AssetManager;
|
|
974
|
+
am.open({
|
|
975
|
+
target,
|
|
976
|
+
types: ["image"],
|
|
977
|
+
select(asset, complete) {
|
|
978
|
+
target.set({ src: asset.getSrc() });
|
|
979
|
+
if (complete) am.close();
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
});
|
|
261
984
|
editor.Commands.add("open-comments", {
|
|
262
985
|
run(ed) {
|
|
263
986
|
const target = targetOf(ed.getSelected() ?? void 0);
|
|
@@ -265,6 +988,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
265
988
|
}
|
|
266
989
|
});
|
|
267
990
|
setupRichText(editor, labels);
|
|
991
|
+
setupStyleFields(editor, labels);
|
|
268
992
|
const blockCategory = (label) => {
|
|
269
993
|
const l = label.toLowerCase();
|
|
270
994
|
if (/(column|section)/.test(l)) return "Layout";
|
|
@@ -293,6 +1017,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
293
1017
|
if (selectionSource.current === "canvas") setSidebarView("settings");
|
|
294
1018
|
selectionSource.current = "other";
|
|
295
1019
|
buildCrumbs(c);
|
|
1020
|
+
setSelectedImageSrc(c && tagOf(c) === "mj-image" ? String(c.get("src") ?? "") : null);
|
|
296
1021
|
if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
|
|
297
1022
|
c.addTrait(
|
|
298
1023
|
{
|
|
@@ -306,40 +1031,99 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
306
1031
|
);
|
|
307
1032
|
}
|
|
308
1033
|
});
|
|
309
|
-
editor.on("component:deselected", () =>
|
|
1034
|
+
editor.on("component:deselected", () => {
|
|
1035
|
+
const rest = editor.getSelected();
|
|
1036
|
+
buildCrumbs(rest);
|
|
1037
|
+
setSelectedImageSrc(
|
|
1038
|
+
rest && tagOf(rest) === "mj-image" ? String(rest.get("src") ?? "") : null
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
editor.on("component:update:src", (c) => {
|
|
1042
|
+
if (c === editor.getSelected()) setSelectedImageSrc(String(c.get("src") ?? ""));
|
|
1043
|
+
});
|
|
1044
|
+
let stored = "";
|
|
310
1045
|
const save = async () => {
|
|
311
|
-
|
|
1046
|
+
const view = editor.getEditing()?.getView();
|
|
1047
|
+
await view?.syncContent?.();
|
|
1048
|
+
const mjml = editor.getHtml();
|
|
1049
|
+
if (mjml === stored) return;
|
|
312
1050
|
setSave("saving");
|
|
313
1051
|
try {
|
|
314
|
-
await documents.save(docId, {
|
|
315
|
-
|
|
316
|
-
projectData: editor.getProjectData()
|
|
317
|
-
});
|
|
1052
|
+
await documents.save(docId, { mjml, projectData: editor.getProjectData() });
|
|
1053
|
+
stored = mjml;
|
|
318
1054
|
setSave("saved");
|
|
319
1055
|
} catch (e) {
|
|
320
1056
|
setSave("error");
|
|
321
|
-
|
|
1057
|
+
toast2.error(refusal(e) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
|
|
322
1058
|
throw e;
|
|
323
1059
|
}
|
|
324
1060
|
};
|
|
1061
|
+
const queue = createSaveQueue(save, 1200);
|
|
325
1062
|
editor.on("update", () => {
|
|
326
1063
|
notifyState();
|
|
327
|
-
if (loadingRef.current)
|
|
328
|
-
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
329
|
-
saveTimer.current = setTimeout(() => void save().catch(console.error), 1200);
|
|
1064
|
+
if (!loadingRef.current && !readOnlyRef.current) queue.schedule();
|
|
330
1065
|
});
|
|
331
1066
|
editor.on("change:device", notifyState);
|
|
1067
|
+
const blocked = [
|
|
1068
|
+
"core:undo",
|
|
1069
|
+
"core:redo",
|
|
1070
|
+
"core:paste",
|
|
1071
|
+
"core:component-delete",
|
|
1072
|
+
"tlb-delete",
|
|
1073
|
+
"tlb-clone",
|
|
1074
|
+
"tlb-move",
|
|
1075
|
+
"open-assets",
|
|
1076
|
+
"open-image-picker"
|
|
1077
|
+
];
|
|
1078
|
+
for (const id of blocked) {
|
|
1079
|
+
editor.on(`command:run:before:${id}`, ({ options }) => {
|
|
1080
|
+
if (readOnlyRef.current) options.abort = true;
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const lock = (on) => {
|
|
1084
|
+
readOnlyRef.current = on;
|
|
1085
|
+
setLocked(on);
|
|
1086
|
+
notifyState();
|
|
1087
|
+
};
|
|
1088
|
+
applyReadOnly.current = async (on) => {
|
|
1089
|
+
if (on) {
|
|
1090
|
+
lock(true);
|
|
1091
|
+
queue.cancel();
|
|
1092
|
+
editor.select([]);
|
|
1093
|
+
editor.AssetManager.close();
|
|
1094
|
+
setPicker(null);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
await reload();
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
console.error(e);
|
|
1101
|
+
toast2.error(labels.documentLoadFailed, {
|
|
1102
|
+
id: `reload-${docId}`,
|
|
1103
|
+
duration: Infinity,
|
|
1104
|
+
action: { label: labels.retry, onClick: () => void applyReadOnly.current(false) }
|
|
1105
|
+
});
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
lock(false);
|
|
1109
|
+
};
|
|
1110
|
+
const reload = () => queue.run(async () => {
|
|
1111
|
+
const fresh = await documents.get(docId);
|
|
1112
|
+
if (fresh.mjml !== stored) loadMjml(fresh.mjml);
|
|
1113
|
+
});
|
|
332
1114
|
const loadMjml = (mjml) => {
|
|
333
1115
|
loadingRef.current = true;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
editor.getWrapper()?.find(
|
|
339
|
-
|
|
340
|
-
|
|
1116
|
+
try {
|
|
1117
|
+
queue.cancel();
|
|
1118
|
+
stored = mjml;
|
|
1119
|
+
editor.setComponents(mjml || STARTER_MJML);
|
|
1120
|
+
editor.getWrapper()?.find("mj-section").forEach(decorate);
|
|
1121
|
+
for (const t of Object.keys(TYPE_LABEL)) {
|
|
1122
|
+
editor.getWrapper()?.find(t).forEach(decorate);
|
|
1123
|
+
}
|
|
1124
|
+
} finally {
|
|
341
1125
|
loadingRef.current = false;
|
|
342
|
-
}
|
|
1126
|
+
}
|
|
343
1127
|
};
|
|
344
1128
|
const ensureHighlightStyles = () => {
|
|
345
1129
|
const cdoc = editor.Canvas.getDocument();
|
|
@@ -376,7 +1160,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
376
1160
|
const doc = await documents.get(docId);
|
|
377
1161
|
loadMjml(doc.mjml);
|
|
378
1162
|
} catch (e) {
|
|
379
|
-
|
|
1163
|
+
toast2.error(labels.documentLoadFailed);
|
|
380
1164
|
console.error(e);
|
|
381
1165
|
} finally {
|
|
382
1166
|
setLoading(false);
|
|
@@ -384,14 +1168,8 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
384
1168
|
onReady({
|
|
385
1169
|
getMjml: () => editor.getHtml(),
|
|
386
1170
|
getCompiledHtml: compiledHtml,
|
|
387
|
-
flushSave:
|
|
388
|
-
|
|
389
|
-
await save();
|
|
390
|
-
},
|
|
391
|
-
reloadFromDb: async () => {
|
|
392
|
-
const fresh = await documents.get(docId);
|
|
393
|
-
if (fresh.mjml !== editor.getHtml()) loadMjml(fresh.mjml);
|
|
394
|
-
},
|
|
1171
|
+
flushSave: () => queue.flush(),
|
|
1172
|
+
reloadFromDb: reload,
|
|
395
1173
|
highlightSection: (sectionId, on) => {
|
|
396
1174
|
ensureHighlightStyles();
|
|
397
1175
|
const el = sectionEl(sectionId);
|
|
@@ -407,10 +1185,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
407
1185
|
notifyState();
|
|
408
1186
|
},
|
|
409
1187
|
undo: () => {
|
|
1188
|
+
if (readOnlyRef.current) return;
|
|
410
1189
|
editor.UndoManager.undo();
|
|
411
1190
|
notifyState();
|
|
412
1191
|
},
|
|
413
1192
|
redo: () => {
|
|
1193
|
+
if (readOnlyRef.current) return;
|
|
414
1194
|
editor.UndoManager.redo();
|
|
415
1195
|
notifyState();
|
|
416
1196
|
},
|
|
@@ -419,6 +1199,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
419
1199
|
notifyState();
|
|
420
1200
|
},
|
|
421
1201
|
setContentWidth: (w) => {
|
|
1202
|
+
if (readOnlyRef.current) return;
|
|
422
1203
|
findByTag("mj-body")?.addAttributes({ width: w });
|
|
423
1204
|
notifyState();
|
|
424
1205
|
},
|
|
@@ -429,7 +1210,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
429
1210
|
}
|
|
430
1211
|
});
|
|
431
1212
|
};
|
|
432
|
-
return /* @__PURE__ */
|
|
1213
|
+
return /* @__PURE__ */ jsx5(
|
|
433
1214
|
GjsEditor,
|
|
434
1215
|
{
|
|
435
1216
|
grapesjs,
|
|
@@ -443,6 +1224,19 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
443
1224
|
styleManager: { appendTo: "#gjs-styles" },
|
|
444
1225
|
traitManager: { appendTo: "#gjs-traits" },
|
|
445
1226
|
layerManager: { appendTo: "#gjs-layers" },
|
|
1227
|
+
assetManager: {
|
|
1228
|
+
// Never inline files: mail clients block `data:` images (see ImageLibrary).
|
|
1229
|
+
embedAsBase64: false,
|
|
1230
|
+
// Our own dialog instead of GrapesJS's modal. GrapesJS still decides when it opens
|
|
1231
|
+
// and what a choice applies to; `select(url, true)` sets the image and closes.
|
|
1232
|
+
custom: {
|
|
1233
|
+
open: (props) => setPicker({
|
|
1234
|
+
current: props.options?.current ?? (String(props.options?.target?.get("src") ?? "") || void 0),
|
|
1235
|
+
select: (url) => props.select(url, true)
|
|
1236
|
+
}),
|
|
1237
|
+
close: () => setPicker(null)
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
446
1240
|
deviceManager: {
|
|
447
1241
|
devices: [
|
|
448
1242
|
{ id: "desktop", name: "Desktop", width: "" },
|
|
@@ -452,19 +1246,26 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
452
1246
|
plugins: [grapesjsMJML]
|
|
453
1247
|
},
|
|
454
1248
|
onEditor,
|
|
455
|
-
children: /* @__PURE__ */
|
|
456
|
-
/* @__PURE__ */
|
|
1249
|
+
children: /* @__PURE__ */ jsxs4("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
|
|
1250
|
+
readOnly && /* @__PURE__ */ jsx5("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
|
|
1251
|
+
/* @__PURE__ */ jsx5(Spinner, {}),
|
|
1252
|
+
" ",
|
|
1253
|
+
labels.agentWorking
|
|
1254
|
+
] }) }),
|
|
1255
|
+
/* @__PURE__ */ jsx5(
|
|
457
1256
|
LeftSidebar,
|
|
458
1257
|
{
|
|
459
1258
|
view: sidebarView,
|
|
460
1259
|
onViewChange: setSidebarView,
|
|
461
|
-
markLayersSource: () => selectionSource.current = "layers"
|
|
1260
|
+
markLayersSource: () => selectionSource.current = "layers",
|
|
1261
|
+
selectedImageSrc,
|
|
1262
|
+
onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
|
|
462
1263
|
}
|
|
463
1264
|
),
|
|
464
|
-
/* @__PURE__ */
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
-
/* @__PURE__ */
|
|
467
|
-
/* @__PURE__ */
|
|
1265
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
|
|
1266
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative min-h-0 flex-1", children: [
|
|
1267
|
+
/* @__PURE__ */ jsx5(Canvas, { className: "h-full" }),
|
|
1268
|
+
/* @__PURE__ */ jsx5(
|
|
468
1269
|
CanvasComments,
|
|
469
1270
|
{
|
|
470
1271
|
docId,
|
|
@@ -474,12 +1275,27 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
|
|
|
474
1275
|
onOpenCountChange
|
|
475
1276
|
}
|
|
476
1277
|
),
|
|
477
|
-
|
|
478
|
-
|
|
1278
|
+
/* @__PURE__ */ jsx5(
|
|
1279
|
+
ImagePicker,
|
|
1280
|
+
{
|
|
1281
|
+
open: picker !== null,
|
|
1282
|
+
currentUrl: picker?.current,
|
|
1283
|
+
onSelect: (url) => {
|
|
1284
|
+
if (!readOnlyRef.current) picker?.select(url);
|
|
1285
|
+
setPicker(null);
|
|
1286
|
+
},
|
|
1287
|
+
onClose: () => {
|
|
1288
|
+
setPicker(null);
|
|
1289
|
+
editorRef.current?.AssetManager.close();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
),
|
|
1293
|
+
loading && /* @__PURE__ */ jsxs4("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
|
|
1294
|
+
/* @__PURE__ */ jsx5(Spinner, {}),
|
|
479
1295
|
" Loading editor\u2026"
|
|
480
1296
|
] })
|
|
481
1297
|
] }),
|
|
482
|
-
/* @__PURE__ */
|
|
1298
|
+
/* @__PURE__ */ jsx5(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
|
|
483
1299
|
] })
|
|
484
1300
|
] })
|
|
485
1301
|
}
|
|
@@ -490,32 +1306,34 @@ function CollapseSection({
|
|
|
490
1306
|
defaultOpen = true,
|
|
491
1307
|
children
|
|
492
1308
|
}) {
|
|
493
|
-
const [open, setOpen] =
|
|
494
|
-
return /* @__PURE__ */
|
|
495
|
-
/* @__PURE__ */
|
|
1309
|
+
const [open, setOpen] = useState3(defaultOpen);
|
|
1310
|
+
return /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border", children: [
|
|
1311
|
+
/* @__PURE__ */ jsxs4(
|
|
496
1312
|
"button",
|
|
497
1313
|
{
|
|
498
1314
|
type: "button",
|
|
499
1315
|
onClick: () => setOpen((o) => !o),
|
|
500
1316
|
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",
|
|
501
1317
|
children: [
|
|
502
|
-
/* @__PURE__ */
|
|
1318
|
+
/* @__PURE__ */ jsx5(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
|
|
503
1319
|
title
|
|
504
1320
|
]
|
|
505
1321
|
}
|
|
506
1322
|
),
|
|
507
|
-
/* @__PURE__ */
|
|
1323
|
+
/* @__PURE__ */ jsx5("div", { className: cn(!open && "hidden"), children })
|
|
508
1324
|
] });
|
|
509
1325
|
}
|
|
510
1326
|
function LeftSidebar({
|
|
511
1327
|
view,
|
|
512
1328
|
onViewChange,
|
|
513
|
-
markLayersSource
|
|
1329
|
+
markLayersSource,
|
|
1330
|
+
selectedImageSrc,
|
|
1331
|
+
onChooseImage
|
|
514
1332
|
}) {
|
|
515
1333
|
const editor = useEditorMaybe();
|
|
516
1334
|
const labels = useLabels();
|
|
517
|
-
const [layersExpanded, setLayersExpanded] =
|
|
518
|
-
const [blockSearch, setBlockSearch] =
|
|
1335
|
+
const [layersExpanded, setLayersExpanded] = useState3(false);
|
|
1336
|
+
const [blockSearch, setBlockSearch] = useState3("");
|
|
519
1337
|
const changeView = (v) => {
|
|
520
1338
|
if (v === "settings" && editor && !editor.getSelected()) {
|
|
521
1339
|
const wrapper = editor.getWrapper();
|
|
@@ -544,12 +1362,12 @@ function LeftSidebar({
|
|
|
544
1362
|
cat.style.display = anyVisible ? "" : "none";
|
|
545
1363
|
});
|
|
546
1364
|
};
|
|
547
|
-
return /* @__PURE__ */
|
|
548
|
-
/* @__PURE__ */
|
|
1365
|
+
return /* @__PURE__ */ jsxs4("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
|
|
1366
|
+
/* @__PURE__ */ jsx5("div", { className: "flex border-b border-panel-border", children: [
|
|
549
1367
|
["blocks", "Blocks"],
|
|
550
1368
|
["settings", "Settings"],
|
|
551
1369
|
["layers", "Layers"]
|
|
552
|
-
].map(([value, label]) => /* @__PURE__ */
|
|
1370
|
+
].map(([value, label]) => /* @__PURE__ */ jsx5(
|
|
553
1371
|
"button",
|
|
554
1372
|
{
|
|
555
1373
|
type: "button",
|
|
@@ -563,10 +1381,10 @@ function LeftSidebar({
|
|
|
563
1381
|
},
|
|
564
1382
|
value
|
|
565
1383
|
)) }),
|
|
566
|
-
/* @__PURE__ */
|
|
567
|
-
/* @__PURE__ */
|
|
568
|
-
/* @__PURE__ */
|
|
569
|
-
/* @__PURE__ */
|
|
1384
|
+
/* @__PURE__ */ jsxs4("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
|
|
1385
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative border-b border-panel-border p-2", children: [
|
|
1386
|
+
/* @__PURE__ */ jsx5(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
|
|
1387
|
+
/* @__PURE__ */ jsx5(
|
|
570
1388
|
Input,
|
|
571
1389
|
{
|
|
572
1390
|
value: blockSearch,
|
|
@@ -575,23 +1393,58 @@ function LeftSidebar({
|
|
|
575
1393
|
className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
|
|
576
1394
|
}
|
|
577
1395
|
),
|
|
578
|
-
/* @__PURE__ */
|
|
1396
|
+
/* @__PURE__ */ jsx5("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" })
|
|
579
1397
|
] }),
|
|
580
|
-
/* @__PURE__ */
|
|
1398
|
+
/* @__PURE__ */ jsx5("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
581
1399
|
] }),
|
|
582
|
-
/* @__PURE__ */
|
|
583
|
-
|
|
584
|
-
|
|
1400
|
+
/* @__PURE__ */ jsxs4("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
|
|
1401
|
+
selectedImageSrc !== null && /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border p-3", children: [
|
|
1402
|
+
/* @__PURE__ */ jsxs4(
|
|
1403
|
+
"button",
|
|
1404
|
+
{
|
|
1405
|
+
type: "button",
|
|
1406
|
+
onClick: onChooseImage,
|
|
1407
|
+
title: labels.chooseImage,
|
|
1408
|
+
className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
|
|
1409
|
+
children: [
|
|
1410
|
+
selectedImageSrc ? /* @__PURE__ */ jsx5(
|
|
1411
|
+
"img",
|
|
1412
|
+
{
|
|
1413
|
+
src: selectedImageSrc,
|
|
1414
|
+
alt: "",
|
|
1415
|
+
className: "h-32 w-full bg-black/25 object-contain"
|
|
1416
|
+
}
|
|
1417
|
+
) : /* @__PURE__ */ jsx5("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ jsx5(ImageIcon2, { className: "size-8" }) }),
|
|
1418
|
+
/* @__PURE__ */ jsx5("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 })
|
|
1419
|
+
]
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
/* @__PURE__ */ jsxs4(
|
|
1423
|
+
Button,
|
|
1424
|
+
{
|
|
1425
|
+
type: "button",
|
|
1426
|
+
size: "sm",
|
|
1427
|
+
className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
|
|
1428
|
+
onClick: onChooseImage,
|
|
1429
|
+
children: [
|
|
1430
|
+
/* @__PURE__ */ jsx5(ImageUp2, {}),
|
|
1431
|
+
labels.chooseImage
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1434
|
+
)
|
|
1435
|
+
] }),
|
|
1436
|
+
/* @__PURE__ */ jsx5(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ jsx5("div", { id: "gjs-traits" }) }),
|
|
1437
|
+
/* @__PURE__ */ jsx5(CollapseSection, { title: labels.style, children: /* @__PURE__ */ jsx5("div", { id: "gjs-styles" }) })
|
|
585
1438
|
] }),
|
|
586
|
-
/* @__PURE__ */
|
|
1439
|
+
/* @__PURE__ */ jsxs4(
|
|
587
1440
|
"div",
|
|
588
1441
|
{
|
|
589
1442
|
className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
|
|
590
1443
|
onMouseDownCapture: markLayersSource,
|
|
591
1444
|
children: [
|
|
592
|
-
/* @__PURE__ */
|
|
593
|
-
/* @__PURE__ */
|
|
594
|
-
/* @__PURE__ */
|
|
1445
|
+
/* @__PURE__ */ jsxs4("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: [
|
|
1446
|
+
/* @__PURE__ */ jsx5("span", { children: labels.expandAll }),
|
|
1447
|
+
/* @__PURE__ */ jsx5(
|
|
595
1448
|
Switch,
|
|
596
1449
|
{
|
|
597
1450
|
checked: layersExpanded,
|
|
@@ -600,7 +1453,7 @@ function LeftSidebar({
|
|
|
600
1453
|
}
|
|
601
1454
|
)
|
|
602
1455
|
] }),
|
|
603
|
-
/* @__PURE__ */
|
|
1456
|
+
/* @__PURE__ */ jsx5("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
|
|
604
1457
|
]
|
|
605
1458
|
}
|
|
606
1459
|
)
|
|
@@ -611,9 +1464,9 @@ function Breadcrumb({
|
|
|
611
1464
|
onSelect
|
|
612
1465
|
}) {
|
|
613
1466
|
const labels = useLabels();
|
|
614
|
-
return /* @__PURE__ */
|
|
615
|
-
i > 0 && /* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
1467
|
+
return /* @__PURE__ */ jsx5("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__ */ jsx5("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-0.5", children: [
|
|
1468
|
+
i > 0 && /* @__PURE__ */ jsx5(ChevronRight, { className: "size-3 opacity-40" }),
|
|
1469
|
+
/* @__PURE__ */ jsx5(
|
|
617
1470
|
"button",
|
|
618
1471
|
{
|
|
619
1472
|
type: "button",
|