@mjml-agent-editor/editor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,7 +116,8 @@ const images: ImageLibrary = {
116
116
 
117
117
  `quota` (`{ used, limit }`) is shown in the picker and stops uploads when reached. Leave
118
118
  `remove` out for a read-only gallery — a host that caps uploads but cannot reclaim the
119
- storage would otherwise be handing out a way round its own cap.
119
+ storage would otherwise be handing out a way round its own cap. Leave `upload` out, and set
120
+ `allowUrl: false`, for a gallery of your own images and nothing else.
120
121
 
121
122
  Without a library the picker takes URLs only. The editor never inlines a file as base64:
122
123
  most mail clients block `data:` images, and every agent turn would pay for the bytes.
package/dist/canvas.cjs CHANGED
@@ -1124,7 +1124,8 @@ function ImagePicker({
1124
1124
  const accepts = (file) => matchesAccept(file, library?.accept);
1125
1125
  const full = quota !== void 0 && quota.used >= quota.limit;
1126
1126
  const upload = async (files) => {
1127
- if (!library || !files?.length) return;
1127
+ const send = library?.upload?.bind(library);
1128
+ if (!send || !files?.length) return;
1128
1129
  const mine = session.current;
1129
1130
  const list = Array.from(files).filter((f) => {
1130
1131
  if (accepts(f)) return true;
@@ -1133,7 +1134,7 @@ function ImagePicker({
1133
1134
  });
1134
1135
  if (!list.length) return;
1135
1136
  setPending((n) => n + list.length);
1136
- const results = await Promise.allSettled(list.map(async (f) => library.upload(f)));
1137
+ const results = await Promise.allSettled(list.map(async (f) => send(f)));
1137
1138
  setPending((n) => n - list.length);
1138
1139
  for (const r of results) {
1139
1140
  if (r.status === "rejected") {
@@ -1168,155 +1169,163 @@ function ImagePicker({
1168
1169
  setDragging(false);
1169
1170
  if (!full) void upload(e.dataTransfer.files);
1170
1171
  };
1171
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.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: [
1172
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
1173
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
1174
- quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
1175
- ] }),
1176
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
1177
- library && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsxs)(
1178
- "div",
1179
- {
1180
- onDragOver: (e) => {
1181
- e.preventDefault();
1182
- setDragging(true);
1183
- },
1184
- onDragLeave: () => setDragging(false),
1185
- onDrop,
1186
- className: cn(
1187
- "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
1188
- dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
1189
- ),
1190
- children: [
1191
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
1192
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
1193
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
1194
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1195
- Button,
1196
- {
1197
- type: "button",
1198
- size: "sm",
1199
- className: "bg-brand text-brand-fg hover:bg-brand/90",
1200
- onClick: () => fileInput.current?.click(),
1201
- children: labels.imagePickerBrowse
1202
- }
1203
- )
1204
- ] }),
1205
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1206
- "input",
1207
- {
1208
- ref: fileInput,
1209
- type: "file",
1210
- accept: library.accept ?? "image/*",
1211
- multiple: true,
1212
- hidden: true,
1213
- onChange: (e) => {
1214
- void upload(e.target.files);
1215
- e.target.value = "";
1216
- }
1217
- }
1218
- )
1219
- ]
1220
- }
1221
- )),
1222
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1223
- "form",
1224
- {
1225
- className: "flex gap-2",
1226
- onSubmit: (e) => {
1227
- e.preventDefault();
1228
- const value = url.trim();
1229
- if (value) onSelect(value);
1230
- },
1231
- children: [
1232
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
1233
- /* @__PURE__ */ (0, import_jsx_runtime11.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" }),
1234
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1235
- Input,
1236
- {
1237
- value: url,
1238
- onChange: (e) => setUrl(e.target.value),
1239
- placeholder: labels.imagePickerUrlPlaceholder,
1240
- className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
1241
- }
1242
- )
1243
- ] }),
1244
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1245
- Button,
1246
- {
1247
- type: "submit",
1248
- size: "lg",
1249
- variant: "outline",
1250
- disabled: !url.trim(),
1251
- className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
1252
- children: labels.imagePickerUseUrl
1253
- }
1254
- )
1255
- ]
1256
- }
1257
- ),
1258
- library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
1259
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
1260
- loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
1261
- Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1262
- "li",
1172
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1173
+ DialogContent,
1174
+ {
1175
+ onDragOver: (e) => e.preventDefault(),
1176
+ onDrop: (e) => e.preventDefault(),
1177
+ className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl",
1178
+ children: [
1179
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
1180
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
1181
+ quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
1182
+ ] }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
1184
+ library?.upload && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsxs)(
1185
+ "div",
1263
1186
  {
1264
- className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
1265
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
1266
- },
1267
- `pending-${i}`
1187
+ onDragOver: (e) => {
1188
+ e.preventDefault();
1189
+ setDragging(true);
1190
+ },
1191
+ onDragLeave: () => setDragging(false),
1192
+ onDrop,
1193
+ className: cn(
1194
+ "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
1195
+ dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
1196
+ ),
1197
+ children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
1200
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
1201
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1202
+ Button,
1203
+ {
1204
+ type: "button",
1205
+ size: "sm",
1206
+ className: "bg-brand text-brand-fg hover:bg-brand/90",
1207
+ onClick: () => fileInput.current?.click(),
1208
+ children: labels.imagePickerBrowse
1209
+ }
1210
+ )
1211
+ ] }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1213
+ "input",
1214
+ {
1215
+ ref: fileInput,
1216
+ type: "file",
1217
+ accept: library.accept ?? "image/*",
1218
+ multiple: true,
1219
+ hidden: true,
1220
+ onChange: (e) => {
1221
+ void upload(e.target.files);
1222
+ e.target.value = "";
1223
+ }
1224
+ }
1225
+ )
1226
+ ]
1227
+ }
1268
1228
  )),
1269
- images.map((image) => {
1270
- const selected = image.url === currentUrl;
1271
- const confirming = confirmId === image.id;
1272
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
1273
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1274
- "button",
1275
- {
1276
- type: "button",
1277
- onClick: () => onSelect(image.url),
1278
- title: image.name,
1279
- className: cn(
1280
- "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",
1281
- selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
1282
- ),
1283
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1284
- "img",
1229
+ library?.allowUrl !== false && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1230
+ "form",
1231
+ {
1232
+ className: "flex gap-2",
1233
+ onSubmit: (e) => {
1234
+ e.preventDefault();
1235
+ const value = url.trim();
1236
+ if (value) onSelect(value);
1237
+ },
1238
+ children: [
1239
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
1240
+ /* @__PURE__ */ (0, import_jsx_runtime11.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" }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1242
+ Input,
1285
1243
  {
1286
- src: image.url,
1287
- alt: image.name ?? "",
1288
- loading: "lazy",
1289
- className: "size-full object-contain"
1244
+ value: url,
1245
+ onChange: (e) => setUrl(e.target.value),
1246
+ placeholder: labels.imagePickerUrlPlaceholder,
1247
+ className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
1290
1248
  }
1291
1249
  )
1292
- }
1293
- ),
1294
- selected && /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
1295
- image.name && /* @__PURE__ */ (0, import_jsx_runtime11.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 }),
1296
- library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1297
- "button",
1250
+ ] }),
1251
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1252
+ Button,
1253
+ {
1254
+ type: "submit",
1255
+ size: "lg",
1256
+ variant: "outline",
1257
+ disabled: !url.trim(),
1258
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
1259
+ children: labels.imagePickerUseUrl
1260
+ }
1261
+ )
1262
+ ]
1263
+ }
1264
+ ),
1265
+ library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
1266
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
1267
+ loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
1268
+ Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1269
+ "li",
1298
1270
  {
1299
- type: "button",
1300
- onClick: () => void remove(image),
1301
- onBlur: () => confirming && setConfirmId(null),
1302
- title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
1303
- "aria-label": labels.imagePickerDelete,
1304
- className: cn(
1305
- "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",
1306
- confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
1271
+ className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
1272
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
1273
+ },
1274
+ `pending-${i}`
1275
+ )),
1276
+ images.map((image) => {
1277
+ const selected = image.url === currentUrl;
1278
+ const confirming = confirmId === image.id;
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1281
+ "button",
1282
+ {
1283
+ type: "button",
1284
+ onClick: () => onSelect(image.url),
1285
+ title: image.name,
1286
+ className: cn(
1287
+ "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",
1288
+ selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
1289
+ ),
1290
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1291
+ "img",
1292
+ {
1293
+ src: image.url,
1294
+ alt: image.name ?? "",
1295
+ loading: "lazy",
1296
+ className: "size-full object-contain"
1297
+ }
1298
+ )
1299
+ }
1307
1300
  ),
1308
- children: [
1309
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
1310
- confirming && labels.imagePickerConfirmDelete
1311
- ]
1312
- }
1313
- )
1314
- ] }, image.id);
1315
- })
1301
+ selected && /* @__PURE__ */ (0, import_jsx_runtime11.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_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
1302
+ image.name && /* @__PURE__ */ (0, import_jsx_runtime11.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 }),
1303
+ library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1304
+ "button",
1305
+ {
1306
+ type: "button",
1307
+ onClick: () => void remove(image),
1308
+ onBlur: () => confirming && setConfirmId(null),
1309
+ title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
1310
+ "aria-label": labels.imagePickerDelete,
1311
+ className: cn(
1312
+ "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",
1313
+ confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
1314
+ ),
1315
+ children: [
1316
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
1317
+ confirming && labels.imagePickerConfirmDelete
1318
+ ]
1319
+ }
1320
+ )
1321
+ ] }, image.id);
1322
+ })
1323
+ ] })
1324
+ ] })
1316
1325
  ] })
1317
- ] })
1318
- ] })
1319
- ] }) });
1326
+ ]
1327
+ }
1328
+ ) });
1320
1329
  }
1321
1330
  function matchesAccept(file, accept) {
1322
1331
  if (!accept) return file.type.startsWith("image/");