@fonderie/vue-courier-admin-screens 0.6.0 → 0.7.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/dist/index.cjs CHANGED
@@ -20,13 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ TemplateCreateScreen: () => TemplateCreateScreen,
23
24
  TemplateEditorScreen: () => TemplateEditorScreen,
24
25
  TemplateListScreen: () => TemplateListScreen
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
28
- // src/screens/TemplateEditorScreen.ts
29
- var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
29
+ // src/screens/TemplateCreateScreen.ts
30
+ var import_client = require("@fonderie/client");
30
31
  var import_vue = require("vue");
31
32
 
32
33
  // src/styles.ts
@@ -265,18 +266,170 @@ var styles = {
265
266
  dot: { width: "6px", height: "6px", borderRadius: "999px", background: "currentColor" }
266
267
  };
267
268
 
269
+ // src/screens/TemplateCreateScreen.ts
270
+ var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
271
+ var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
272
+ var TemplateCreateScreen = (0, import_vue.defineComponent)({
273
+ name: "FonderieTemplateCreateScreen",
274
+ props: {
275
+ client: { type: Object, required: true },
276
+ /**
277
+ * Given ⇒ add a LOCALE of this existing template: the type is fixed and the
278
+ * form starts from its default-locale copy. Omitted ⇒ a brand-new template
279
+ * type (one the app sends that has no stored copy yet).
280
+ */
281
+ type: { type: String, default: void 0 },
282
+ /** Locales already in use, offered as suggestions. */
283
+ locales: { type: Array, default: () => [] }
284
+ },
285
+ emits: {
286
+ /** What was created, so the caller can open it in the editor. */
287
+ created: (_created) => true
288
+ },
289
+ setup(props, { emit }) {
290
+ const addingLocale = props.type !== void 0;
291
+ const newType = (0, import_vue.ref)("");
292
+ const locale = (0, import_vue.ref)("");
293
+ const subject = (0, import_vue.ref)("");
294
+ const html = (0, import_vue.ref)("");
295
+ const text = (0, import_vue.ref)("");
296
+ const error = (0, import_vue.ref)(null);
297
+ const busy = (0, import_vue.ref)(false);
298
+ (0, import_vue.onMounted)(() => {
299
+ if (!props.type) return;
300
+ props.client.getTemplate(props.type, null).then(({ result }) => {
301
+ subject.value = result.subject ?? "";
302
+ html.value = result.html ?? "";
303
+ text.value = result.text;
304
+ }).catch(() => void 0);
305
+ });
306
+ async function submit(e) {
307
+ e.preventDefault();
308
+ error.value = null;
309
+ const t = addingLocale ? props.type : newType.value.trim();
310
+ const l = locale.value.trim() || null;
311
+ if (!TYPE_RE.test(t)) {
312
+ error.value = "Type: lowercase letters, digits, - and _ (e.g. weekly-digest).";
313
+ return;
314
+ }
315
+ if (addingLocale && !l) {
316
+ error.value = "Choose the locale to add, e.g. fr or fr-CA.";
317
+ return;
318
+ }
319
+ if (l && !LOCALE_RE.test(l)) {
320
+ error.value = "Locale: a language tag such as fr, es or fr-CA.";
321
+ return;
322
+ }
323
+ if (!text.value.trim()) {
324
+ error.value = "The plain-text body is required \u2014 it is what every email client can show.";
325
+ return;
326
+ }
327
+ busy.value = true;
328
+ try {
329
+ const exists = await props.client.getTemplate(t, l).then(
330
+ () => true,
331
+ (err) => !(err instanceof import_client.FonderieApiError && err.status === 404)
332
+ );
333
+ if (exists) {
334
+ error.value = `"${t}"${l ? ` (${l})` : ""} already exists \u2014 open it from the list to edit.`;
335
+ return;
336
+ }
337
+ const input = { text: text.value, active: true };
338
+ if (subject.value) input.subject = subject.value;
339
+ if (html.value) input.html = html.value;
340
+ await props.client.setTemplate(t, input, l);
341
+ emit("created", { type: t, locale: l });
342
+ } catch (err) {
343
+ error.value = err instanceof import_client.FonderieApiError ? err.explanation : String(err);
344
+ } finally {
345
+ busy.value = false;
346
+ }
347
+ }
348
+ const field = (id, label, model, opts = {}) => [
349
+ (0, import_vue.h)("label", { style: styles.label, for: id }, label),
350
+ (0, import_vue.h)(opts.textarea ? "textarea" : "input", {
351
+ id,
352
+ style: opts.textarea ? { ...styles.textarea, minHeight: opts.minHeight } : styles.input,
353
+ value: model.value,
354
+ autocomplete: "off",
355
+ spellcheck: false,
356
+ ...opts.placeholder ? { placeholder: opts.placeholder } : {},
357
+ ...opts.list ? { list: opts.list } : {},
358
+ onInput: (ev) => {
359
+ model.value = ev.target.value;
360
+ }
361
+ })
362
+ ];
363
+ return () => (0, import_vue.h)("div", { style: { ...styles.container, maxWidth: "760px" } }, [
364
+ (0, import_vue.h)(
365
+ "h1",
366
+ { style: styles.title },
367
+ addingLocale ? `Add a locale to ${props.type}` : "New template"
368
+ ),
369
+ (0, import_vue.h)(
370
+ "p",
371
+ { style: { ...styles.meta, lineHeight: 1.5 } },
372
+ addingLocale ? "Starts from the default copy. People whose locale matches receive this version; everyone else keeps the default." : "For an email your app sends that has no stored copy yet. The type must match what the app sends."
373
+ ),
374
+ (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
375
+ ...addingLocale ? [] : field("new-template-type", "Type", newType, { placeholder: "weekly-digest" }),
376
+ ...field(
377
+ "new-template-locale",
378
+ addingLocale ? "Locale" : "Locale (optional \u2014 empty is the default)",
379
+ locale,
380
+ { placeholder: "fr", list: "new-template-locales" }
381
+ ),
382
+ (0, import_vue.h)(
383
+ "datalist",
384
+ { id: "new-template-locales" },
385
+ [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
386
+ ),
387
+ ...field("new-template-subject", "Subject", subject),
388
+ ...field("new-template-html", "HTML body", html, { textarea: true, minHeight: "160px" }),
389
+ ...field("new-template-text", "Plain-text body", text, {
390
+ textarea: true,
391
+ minHeight: "100px"
392
+ }),
393
+ error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
394
+ (0, import_vue.h)(
395
+ "button",
396
+ {
397
+ type: "submit",
398
+ disabled: busy.value,
399
+ style: { ...styles.button, marginTop: "16px" }
400
+ },
401
+ busy.value ? "Creating\u2026" : addingLocale ? "Add locale" : "Create template"
402
+ )
403
+ ])
404
+ ]);
405
+ }
406
+ });
407
+
268
408
  // src/screens/TemplateEditorScreen.ts
409
+ var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
410
+ var import_vue2 = require("vue");
269
411
  var PREVIEW_DEBOUNCE_MS = 500;
270
412
  var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
271
- var TemplateEditorScreen = (0, import_vue.defineComponent)({
413
+ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
272
414
  name: "FonderieTemplateEditorScreen",
273
415
  props: {
274
416
  client: { type: Object, required: true },
275
417
  type: { type: String, required: true },
276
- locale: { type: String, default: null }
418
+ locale: { type: String, default: null },
419
+ /**
420
+ * A built-in email's default-locale row (the list's `system` flag): it can
421
+ * be edited and rolled back, never deleted — Delete is not offered.
422
+ */
423
+ system: { type: Boolean, default: false },
424
+ /** Shows "+ Add locale" on the default locale (emits add-locale). */
425
+ allowAddLocale: { type: Boolean, default: false }
277
426
  },
278
427
  emits: {
279
- saved: () => true
428
+ saved: () => true,
429
+ /** The template was deleted and no longer exists. */
430
+ deleted: () => true,
431
+ /** Create a translation of this template. */
432
+ "add-locale": (_type) => true
280
433
  },
281
434
  setup(props, { emit }) {
282
435
  const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
@@ -284,15 +437,16 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
284
437
  props.type,
285
438
  props.locale
286
439
  );
287
- const { saveTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
440
+ const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
441
+ const isDeleting = (0, import_vue2.ref)(false);
288
442
  const { revisions, rollback } = (0, import_vue_courier_admin.useTemplateRevisions)(props.client, props.type, props.locale);
289
- const isSaving = (0, import_vue.ref)(false);
290
- const saveError = (0, import_vue.ref)(null);
291
- const subject = (0, import_vue.ref)("");
292
- const html = (0, import_vue.ref)("");
293
- const text = (0, import_vue.ref)("");
294
- const active = (0, import_vue.ref)(true);
295
- (0, import_vue.watch)(template, (tpl) => {
443
+ const isSaving = (0, import_vue2.ref)(false);
444
+ const saveError = (0, import_vue2.ref)(null);
445
+ const subject = (0, import_vue2.ref)("");
446
+ const html = (0, import_vue2.ref)("");
447
+ const text = (0, import_vue2.ref)("");
448
+ const active = (0, import_vue2.ref)(true);
449
+ (0, import_vue2.watch)(template, (tpl) => {
296
450
  if (!tpl) return;
297
451
  subject.value = tpl.subject ?? "";
298
452
  html.value = tpl.html ?? "";
@@ -305,8 +459,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
305
459
  error: previewError,
306
460
  renderPreview
307
461
  } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
308
- const sampleJson = (0, import_vue.ref)("{}");
309
- const sampleError = (0, import_vue.ref)(null);
462
+ const sampleJson = (0, import_vue2.ref)("{}");
463
+ const sampleError = (0, import_vue2.ref)(null);
310
464
  async function run() {
311
465
  let data = {};
312
466
  try {
@@ -341,12 +495,12 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
341
495
  }
342
496
  }
343
497
  let previewTimer;
344
- (0, import_vue.watch)([template, subject, html, text, sampleJson], () => {
498
+ (0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
345
499
  if (!template.value) return;
346
500
  clearTimeout(previewTimer);
347
501
  previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS);
348
502
  });
349
- const dirty = (0, import_vue.computed)(() => {
503
+ const dirty = (0, import_vue2.computed)(() => {
350
504
  const tpl = template.value;
351
505
  return !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
352
506
  });
@@ -368,13 +522,28 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
368
522
  isSaving.value = false;
369
523
  }
370
524
  }
525
+ async function handleDelete() {
526
+ const which = props.locale ? `the ${props.locale} version of "${props.type}"` : `"${props.type}"`;
527
+ const fallback = props.locale ? " People in that locale will receive the default version." : "";
528
+ if (!window.confirm(`Delete ${which}?${fallback}`)) return;
529
+ isDeleting.value = true;
530
+ saveError.value = null;
531
+ try {
532
+ await removeTemplate(props.type, props.locale);
533
+ emit("deleted");
534
+ } catch (err) {
535
+ saveError.value = err;
536
+ } finally {
537
+ isDeleting.value = false;
538
+ }
539
+ }
371
540
  function renderRevision(rev) {
372
- return (0, import_vue.h)("li", { key: rev.version, style: styles.revisionRow }, [
373
- (0, import_vue.h)(
541
+ return (0, import_vue2.h)("li", { key: rev.version, style: styles.revisionRow }, [
542
+ (0, import_vue2.h)(
374
543
  "span",
375
544
  `v${rev.version} \u2014 ${rev.actor ?? "unknown"} \u2014 ${new Date(rev.createdAt).toLocaleString()}`
376
545
  ),
377
- (0, import_vue.h)(
546
+ (0, import_vue2.h)(
378
547
  "button",
379
548
  { type: "button", style: styles.rollbackButton, onClick: () => rollback(rev.version) },
380
549
  "Roll back"
@@ -382,21 +551,36 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
382
551
  ]);
383
552
  }
384
553
  return () => {
385
- if (isLoading.value) return (0, import_vue.h)("p", { style: styles.status }, "Loading template\u2026");
554
+ if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, "Loading template\u2026");
386
555
  if (error.value)
387
- return (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
388
- return (0, import_vue.h)("div", { style: styles.container }, [
389
- (0, import_vue.h)("h1", { style: styles.title }, props.type),
390
- (0, import_vue.h)(
391
- "p",
392
- { style: styles.meta },
393
- `${props.locale ?? "base"} \xB7 v${template.value?.version ?? 1}`
556
+ return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
557
+ return (0, import_vue2.h)("div", { style: styles.container }, [
558
+ (0, import_vue2.h)(
559
+ "div",
560
+ { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
561
+ [
562
+ (0, import_vue2.h)("h1", { style: styles.title }, props.type),
563
+ props.allowAddLocale && !props.locale ? (0, import_vue2.h)(
564
+ "button",
565
+ {
566
+ type: "button",
567
+ style: { ...styles.rollbackButton, marginLeft: "auto" },
568
+ onClick: () => emit("add-locale", props.type)
569
+ },
570
+ "+ Add locale"
571
+ ) : null
572
+ ]
394
573
  ),
395
- (0, import_vue.h)("div", { style: styles.split }, [
396
- (0, import_vue.h)("div", { style: styles.column }, [
397
- (0, import_vue.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
398
- (0, import_vue.h)("label", { style: styles.label, for: "template-subject" }, "Subject"),
399
- (0, import_vue.h)("input", {
574
+ (0, import_vue2.h)("p", { style: styles.meta }, [
575
+ (0, import_vue2.h)("strong", props.locale ?? "default locale"),
576
+ ` \xB7 v${template.value?.version ?? 1}`,
577
+ props.system ? " \xB7 built-in email: edit or roll back, it cannot be deleted" : ""
578
+ ]),
579
+ (0, import_vue2.h)("div", { style: styles.split }, [
580
+ (0, import_vue2.h)("div", { style: styles.column }, [
581
+ (0, import_vue2.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
582
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-subject" }, "Subject"),
583
+ (0, import_vue2.h)("input", {
400
584
  id: "template-subject",
401
585
  style: styles.input,
402
586
  value: subject.value,
@@ -404,8 +588,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
404
588
  subject.value = e.target.value;
405
589
  }
406
590
  }),
407
- (0, import_vue.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
408
- (0, import_vue.h)("textarea", {
591
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
592
+ (0, import_vue2.h)("textarea", {
409
593
  id: "template-html",
410
594
  style: styles.textarea,
411
595
  rows: 8,
@@ -414,8 +598,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
414
598
  html.value = e.target.value;
415
599
  }
416
600
  }),
417
- (0, import_vue.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
418
- (0, import_vue.h)("textarea", {
601
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
602
+ (0, import_vue2.h)("textarea", {
419
603
  id: "template-text",
420
604
  style: styles.textarea,
421
605
  rows: 4,
@@ -425,8 +609,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
425
609
  text.value = e.target.value;
426
610
  }
427
611
  }),
428
- (0, import_vue.h)("label", { style: styles.checkboxLabel }, [
429
- (0, import_vue.h)("input", {
612
+ (0, import_vue2.h)("label", { style: styles.checkboxLabel }, [
613
+ (0, import_vue2.h)("input", {
430
614
  type: "checkbox",
431
615
  checked: active.value,
432
616
  onChange: (e) => {
@@ -435,9 +619,23 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
435
619
  }),
436
620
  "Active"
437
621
  ]),
438
- saveError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
439
- (0, import_vue.h)("div", { style: styles.saveRow }, [
440
- (0, import_vue.h)(
622
+ saveError.value?.reason === "VERSION_CONFLICT" ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, [
623
+ "Someone changed this template since you opened it. Reload to see their change, then edit again. ",
624
+ (0, import_vue2.h)(
625
+ "button",
626
+ {
627
+ type: "button",
628
+ style: styles.rollbackButton,
629
+ onClick: () => {
630
+ saveError.value = null;
631
+ void refresh();
632
+ }
633
+ },
634
+ "Reload"
635
+ )
636
+ ]) : saveError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
637
+ (0, import_vue2.h)("div", { style: styles.saveRow }, [
638
+ (0, import_vue2.h)(
441
639
  "button",
442
640
  {
443
641
  type: "submit",
@@ -446,11 +644,27 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
446
644
  },
447
645
  isSaving.value ? "Saving\u2026" : "Save"
448
646
  ),
449
- !dirty.value && !isSaving.value ? (0, import_vue.h)("span", { style: styles.meta }, "No changes to save") : null
647
+ !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, "No changes to save") : null,
648
+ !props.system ? (0, import_vue2.h)(
649
+ "button",
650
+ {
651
+ type: "button",
652
+ disabled: isDeleting.value,
653
+ onClick: () => void handleDelete(),
654
+ style: {
655
+ ...styles.rollbackButton,
656
+ height: "36px",
657
+ marginLeft: "auto",
658
+ color: "var(--fonderie-danger,#e00)",
659
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 40%, transparent)"
660
+ }
661
+ },
662
+ isDeleting.value ? "Deleting\u2026" : "Delete"
663
+ ) : null
450
664
  ])
451
665
  ]),
452
- (0, import_vue.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
453
- (0, import_vue.h)("textarea", {
666
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
667
+ (0, import_vue2.h)("textarea", {
454
668
  id: "template-sample",
455
669
  style: styles.textarea,
456
670
  rows: 6,
@@ -460,31 +674,31 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
460
674
  sampleJson.value = e.target.value;
461
675
  }
462
676
  }),
463
- sampleError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
677
+ sampleError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
464
678
  ]),
465
- (0, import_vue.h)("div", { style: styles.previewColumn }, [
466
- (0, import_vue.h)("div", { style: styles.previewHeader }, [
467
- (0, import_vue.h)("span", { style: styles.label }, "Preview"),
468
- (0, import_vue.h)("span", { style: styles.meta }, isPreviewing.value ? "Rendering\u2026" : "Live")
679
+ (0, import_vue2.h)("div", { style: styles.previewColumn }, [
680
+ (0, import_vue2.h)("div", { style: styles.previewHeader }, [
681
+ (0, import_vue2.h)("span", { style: styles.label }, "Preview"),
682
+ (0, import_vue2.h)("span", { style: styles.meta }, isPreviewing.value ? "Rendering\u2026" : "Live")
469
683
  ]),
470
- previewError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
471
- preview.value?.subject ? (0, import_vue.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
684
+ previewError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
685
+ preview.value?.subject ? (0, import_vue2.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
472
686
  preview.value?.html ? (
473
687
  // sandbox= — no scripts, no same-origin. Operator-authored HTML
474
688
  // must never execute in the dashboard's origin, where the admin
475
689
  // token lives.
476
- (0, import_vue.h)("iframe", {
690
+ (0, import_vue2.h)("iframe", {
477
691
  title: "Template preview",
478
692
  style: styles.previewFrame,
479
693
  sandbox: "",
480
694
  srcdoc: preview.value.html
481
695
  })
482
- ) : preview.value ? (0, import_vue.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue.h)("p", { style: styles.meta }, "Nothing rendered yet.")
696
+ ) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, "Nothing rendered yet.")
483
697
  ])
484
698
  ]),
485
- revisions.value.length > 0 ? (0, import_vue.h)("div", { style: styles.revisions }, [
486
- (0, import_vue.h)("h2", { style: styles.subtitle }, "History"),
487
- (0, import_vue.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
699
+ revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
700
+ (0, import_vue2.h)("h2", { style: styles.subtitle }, "History"),
701
+ (0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
488
702
  ]) : null
489
703
  ]);
490
704
  };
@@ -493,20 +707,48 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
493
707
 
494
708
  // src/screens/TemplateListScreen.ts
495
709
  var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
496
- var import_vue2 = require("vue");
497
- var TemplateListScreen = (0, import_vue2.defineComponent)({
710
+ var import_vue3 = require("vue");
711
+ var builtIn = {
712
+ fontSize: "11px",
713
+ fontWeight: 500,
714
+ padding: "1px 7px",
715
+ borderRadius: "999px",
716
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
717
+ color: "var(--fonderie-text-muted,#5c5c5c)"
718
+ };
719
+ var newButton = {
720
+ display: "inline-flex",
721
+ alignItems: "center",
722
+ height: "32px",
723
+ boxSizing: "border-box",
724
+ borderRadius: "6px",
725
+ padding: "0 12px",
726
+ fontSize: "13px",
727
+ fontWeight: 500,
728
+ fontFamily: "inherit",
729
+ cursor: "pointer",
730
+ backgroundColor: "var(--fonderie-text,#171717)",
731
+ color: "var(--fonderie-surface,#fff)",
732
+ border: "1px solid var(--fonderie-text,#171717)"
733
+ };
734
+ var TemplateListScreen = (0, import_vue3.defineComponent)({
498
735
  name: "FonderieTemplateListScreen",
499
736
  props: {
500
- client: { type: Object, required: true }
737
+ client: { type: Object, required: true },
738
+ /** Shows a "New template" button (emits create-template). */
739
+ allowCreate: { type: Boolean, default: false }
501
740
  },
502
741
  emits: {
503
- "select-template": (_template) => true
742
+ /** The whole row — type AND locale, and whether it is built-in. */
743
+ "select-template": (_template) => true,
744
+ /** Receives the locales in use, to suggest. */
745
+ "create-template": (_context) => true
504
746
  },
505
747
  setup(props, { emit }) {
506
748
  const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
507
749
  function renderRow(template) {
508
- return (0, import_vue2.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
509
- (0, import_vue2.h)(
750
+ return (0, import_vue3.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
751
+ (0, import_vue3.h)(
510
752
  "button",
511
753
  {
512
754
  type: "button",
@@ -514,29 +756,59 @@ var TemplateListScreen = (0, import_vue2.defineComponent)({
514
756
  onClick: () => emit("select-template", template)
515
757
  },
516
758
  [
517
- (0, import_vue2.h)("span", { style: styles.type }, template.type),
518
- (0, import_vue2.h)("span", { style: styles.locale }, template.locale ?? "default locale"),
519
- (0, import_vue2.h)("span", { style: template.active ? styles.active : styles.inactive }, [
520
- (0, import_vue2.h)("span", { style: styles.dot }),
759
+ (0, import_vue3.h)("span", { style: styles.type }, template.type),
760
+ (0, import_vue3.h)("span", { style: styles.locale }, template.locale ?? "default locale"),
761
+ template.system ? (0, import_vue3.h)("span", { style: builtIn }, "built-in") : null,
762
+ (0, import_vue3.h)("span", { style: template.active ? styles.active : styles.inactive }, [
763
+ (0, import_vue3.h)("span", { style: styles.dot }),
521
764
  template.active ? "active" : "inactive"
522
765
  ])
523
766
  ]
524
767
  )
525
768
  ]);
526
769
  }
527
- return () => (0, import_vue2.h)("div", { style: styles.listContainer }, [
528
- (0, import_vue2.h)("h1", { style: styles.listTitle }, "Templates"),
529
- (0, import_vue2.h)(
770
+ return () => (0, import_vue3.h)("div", { style: styles.listContainer }, [
771
+ (0, import_vue3.h)(
772
+ "div",
773
+ {
774
+ style: {
775
+ display: "flex",
776
+ alignItems: "center",
777
+ justifyContent: "space-between",
778
+ gap: "16px"
779
+ }
780
+ },
781
+ [
782
+ (0, import_vue3.h)("h1", { style: styles.listTitle }, "Templates"),
783
+ props.allowCreate ? (0, import_vue3.h)(
784
+ "button",
785
+ {
786
+ type: "button",
787
+ style: newButton,
788
+ onClick: () => emit("create-template", {
789
+ locales: [
790
+ ...new Set(
791
+ templates.value.map((t) => t.locale).filter((l) => !!l)
792
+ )
793
+ ]
794
+ })
795
+ },
796
+ "New template"
797
+ ) : null
798
+ ]
799
+ ),
800
+ (0, import_vue3.h)(
530
801
  "p",
531
802
  { style: styles.hint },
532
803
  "Every email the app sends. Open one to edit its copy and preview it live."
533
804
  ),
534
- isLoading.value ? (0, import_vue2.h)("p", { style: styles.status }, "Loading templates\u2026") : error.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue2.h)("ul", { style: styles.list }, templates.value.map(renderRow))
805
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, "Loading templates\u2026") : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue3.h)("ul", { style: styles.list }, templates.value.map(renderRow))
535
806
  ]);
536
807
  }
537
808
  });
538
809
  // Annotate the CommonJS export names for ESM import in node:
539
810
  0 && (module.exports = {
811
+ TemplateCreateScreen,
540
812
  TemplateEditorScreen,
541
813
  TemplateListScreen
542
814
  });