@fonderie/vue-courier-admin-screens 0.6.0 → 0.8.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,34 +266,194 @@ 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
+ /** The console language; defaults to English. */
285
+ locale: { type: String, default: void 0 }
286
+ },
287
+ emits: {
288
+ /** What was created, so the caller can open it in the editor. */
289
+ created: (_created) => true
290
+ },
291
+ setup(props, { emit }) {
292
+ const addingLocale = props.type !== void 0;
293
+ const newType = (0, import_vue.ref)("");
294
+ const targetLocale = (0, import_vue.ref)("");
295
+ const subject = (0, import_vue.ref)("");
296
+ const html = (0, import_vue.ref)("");
297
+ const text = (0, import_vue.ref)("");
298
+ const error = (0, import_vue.ref)(null);
299
+ const busy = (0, import_vue.ref)(false);
300
+ const tr = (key, params) => (0, import_client.createAdminT)(props.locale)(key, params);
301
+ (0, import_vue.onMounted)(() => {
302
+ if (!props.type) return;
303
+ props.client.getTemplate(props.type, null).then(({ result }) => {
304
+ subject.value = result.subject ?? "";
305
+ html.value = result.html ?? "";
306
+ text.value = result.text;
307
+ }).catch(() => void 0);
308
+ });
309
+ async function submit(e) {
310
+ e.preventDefault();
311
+ error.value = null;
312
+ const t = addingLocale ? props.type : newType.value.trim();
313
+ const l = targetLocale.value.trim() || null;
314
+ if (!TYPE_RE.test(t)) {
315
+ error.value = tr("templates.create.errorType");
316
+ return;
317
+ }
318
+ if (addingLocale && !l) {
319
+ error.value = tr("templates.create.errorLocaleRequired");
320
+ return;
321
+ }
322
+ if (l && !LOCALE_RE.test(l)) {
323
+ error.value = tr("templates.create.errorLocale");
324
+ return;
325
+ }
326
+ if (!text.value.trim()) {
327
+ error.value = tr("templates.create.errorText");
328
+ return;
329
+ }
330
+ busy.value = true;
331
+ try {
332
+ const exists = await props.client.getTemplate(t, l).then(
333
+ () => true,
334
+ (err) => !(err instanceof import_client.FonderieApiError && err.status === 404)
335
+ );
336
+ if (exists) {
337
+ error.value = l ? tr("templates.create.existsLocale", { type: t, locale: l }) : tr("templates.create.exists", { type: t });
338
+ return;
339
+ }
340
+ const input = { text: text.value, active: true };
341
+ if (subject.value) input.subject = subject.value;
342
+ if (html.value) input.html = html.value;
343
+ await props.client.setTemplate(t, input, l);
344
+ emit("created", { type: t, locale: l });
345
+ } catch (err) {
346
+ error.value = err instanceof import_client.FonderieApiError ? err.explanation : String(err);
347
+ } finally {
348
+ busy.value = false;
349
+ }
350
+ }
351
+ const field = (id, label, model, opts = {}) => [
352
+ (0, import_vue.h)("label", { style: styles.label, for: id }, label),
353
+ (0, import_vue.h)(opts.textarea ? "textarea" : "input", {
354
+ id,
355
+ style: opts.textarea ? { ...styles.textarea, minHeight: opts.minHeight } : styles.input,
356
+ value: model.value,
357
+ autocomplete: "off",
358
+ spellcheck: false,
359
+ ...opts.placeholder ? { placeholder: opts.placeholder } : {},
360
+ ...opts.list ? { list: opts.list } : {},
361
+ onInput: (ev) => {
362
+ model.value = ev.target.value;
363
+ }
364
+ })
365
+ ];
366
+ return () => (0, import_vue.h)("div", { style: { ...styles.container, maxWidth: "760px" } }, [
367
+ (0, import_vue.h)(
368
+ "h1",
369
+ { style: styles.title },
370
+ addingLocale ? tr("templates.create.titleLocale", { type: props.type }) : tr("templates.create.titleNew")
371
+ ),
372
+ (0, import_vue.h)(
373
+ "p",
374
+ { style: { ...styles.meta, lineHeight: 1.5 } },
375
+ addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew")
376
+ ),
377
+ (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
378
+ ...addingLocale ? [] : field("new-template-type", tr("templates.create.type"), newType, { placeholder: "weekly-digest" }),
379
+ ...field(
380
+ "new-template-locale",
381
+ addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional"),
382
+ targetLocale,
383
+ { placeholder: "fr", list: "new-template-locales" }
384
+ ),
385
+ (0, import_vue.h)(
386
+ "datalist",
387
+ { id: "new-template-locales" },
388
+ [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
389
+ ),
390
+ ...field("new-template-subject", tr("templates.subject"), subject),
391
+ ...field("new-template-html", tr("templates.htmlBody"), html, { textarea: true, minHeight: "160px" }),
392
+ ...field("new-template-text", tr("templates.textBody"), text, {
393
+ textarea: true,
394
+ minHeight: "100px"
395
+ }),
396
+ error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
397
+ (0, import_vue.h)(
398
+ "button",
399
+ {
400
+ type: "submit",
401
+ disabled: busy.value,
402
+ style: { ...styles.button, marginTop: "16px" }
403
+ },
404
+ busy.value ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew")
405
+ )
406
+ ])
407
+ ]);
408
+ }
409
+ });
410
+
268
411
  // src/screens/TemplateEditorScreen.ts
412
+ var import_client2 = require("@fonderie/client");
413
+ var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
414
+ var import_vue2 = require("vue");
269
415
  var PREVIEW_DEBOUNCE_MS = 500;
270
416
  var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
271
- var TemplateEditorScreen = (0, import_vue.defineComponent)({
417
+ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
272
418
  name: "FonderieTemplateEditorScreen",
273
419
  props: {
274
420
  client: { type: Object, required: true },
275
421
  type: { type: String, required: true },
276
- locale: { type: String, default: null }
422
+ locale: { type: String, default: null },
423
+ /**
424
+ * A built-in email's default-locale row (the list's `system` flag): it can
425
+ * be edited and rolled back, never deleted — Delete is not offered.
426
+ */
427
+ system: { type: Boolean, default: false },
428
+ /** Shows "+ Add locale" on the default locale (emits add-locale). */
429
+ allowAddLocale: { type: Boolean, default: false },
430
+ /** The console language (`locale` is the template's); defaults to English. */
431
+ uiLocale: { type: String, default: void 0 }
277
432
  },
278
433
  emits: {
279
- saved: () => true
434
+ saved: () => true,
435
+ /** The template was deleted and no longer exists. */
436
+ deleted: () => true,
437
+ /** Create a translation of this template. */
438
+ "add-locale": (_type) => true
280
439
  },
281
440
  setup(props, { emit }) {
441
+ const t = (key, params) => (0, import_client2.createAdminT)(props.uiLocale)(key, params);
282
442
  const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
283
443
  props.client,
284
444
  props.type,
285
445
  props.locale
286
446
  );
287
- const { saveTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
447
+ const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
448
+ const isDeleting = (0, import_vue2.ref)(false);
288
449
  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) => {
450
+ const isSaving = (0, import_vue2.ref)(false);
451
+ const saveError = (0, import_vue2.ref)(null);
452
+ const subject = (0, import_vue2.ref)("");
453
+ const html = (0, import_vue2.ref)("");
454
+ const text = (0, import_vue2.ref)("");
455
+ const active = (0, import_vue2.ref)(true);
456
+ (0, import_vue2.watch)(template, (tpl) => {
296
457
  if (!tpl) return;
297
458
  subject.value = tpl.subject ?? "";
298
459
  html.value = tpl.html ?? "";
@@ -305,20 +466,20 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
305
466
  error: previewError,
306
467
  renderPreview
307
468
  } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
308
- const sampleJson = (0, import_vue.ref)("{}");
309
- const sampleError = (0, import_vue.ref)(null);
469
+ const sampleJson = (0, import_vue2.ref)("{}");
470
+ const sampleError = (0, import_vue2.ref)(null);
310
471
  async function run() {
311
472
  let data = {};
312
473
  try {
313
474
  const parsed = JSON.parse(sampleJson.value || "{}");
314
475
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
315
- sampleError.value = "Sample data must be a JSON object.";
476
+ sampleError.value = "templates.editor.sampleNotObject";
316
477
  return;
317
478
  }
318
479
  data = parsed;
319
480
  sampleError.value = null;
320
481
  } catch {
321
- sampleError.value = "Sample data is not valid JSON.";
482
+ sampleError.value = "templates.editor.sampleInvalid";
322
483
  return;
323
484
  }
324
485
  const result = await renderPreview(
@@ -341,12 +502,12 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
341
502
  }
342
503
  }
343
504
  let previewTimer;
344
- (0, import_vue.watch)([template, subject, html, text, sampleJson], () => {
505
+ (0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
345
506
  if (!template.value) return;
346
507
  clearTimeout(previewTimer);
347
508
  previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS);
348
509
  });
349
- const dirty = (0, import_vue.computed)(() => {
510
+ const dirty = (0, import_vue2.computed)(() => {
350
511
  const tpl = template.value;
351
512
  return !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
352
513
  });
@@ -368,35 +529,64 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
368
529
  isSaving.value = false;
369
530
  }
370
531
  }
532
+ async function handleDelete() {
533
+ const message = props.locale ? t("templates.editor.confirmDeleteLocale", { locale: props.locale, type: props.type }) : t("templates.editor.confirmDelete", { type: props.type });
534
+ if (!window.confirm(message)) return;
535
+ isDeleting.value = true;
536
+ saveError.value = null;
537
+ try {
538
+ await removeTemplate(props.type, props.locale);
539
+ emit("deleted");
540
+ } catch (err) {
541
+ saveError.value = err;
542
+ } finally {
543
+ isDeleting.value = false;
544
+ }
545
+ }
371
546
  function renderRevision(rev) {
372
- return (0, import_vue.h)("li", { key: rev.version, style: styles.revisionRow }, [
373
- (0, import_vue.h)(
547
+ return (0, import_vue2.h)("li", { key: rev.version, style: styles.revisionRow }, [
548
+ (0, import_vue2.h)(
374
549
  "span",
375
- `v${rev.version} \u2014 ${rev.actor ?? "unknown"} \u2014 ${new Date(rev.createdAt).toLocaleString()}`
550
+ `v${rev.version} \u2014 ${rev.actor ?? t("templates.editor.unknownActor")} \u2014 ${(0, import_client2.formatAdminDate)(rev.createdAt, props.uiLocale)}`
376
551
  ),
377
- (0, import_vue.h)(
552
+ (0, import_vue2.h)(
378
553
  "button",
379
554
  { type: "button", style: styles.rollbackButton, onClick: () => rollback(rev.version) },
380
- "Roll back"
555
+ t("templates.editor.rollBack")
381
556
  )
382
557
  ]);
383
558
  }
384
559
  return () => {
385
- if (isLoading.value) return (0, import_vue.h)("p", { style: styles.status }, "Loading template\u2026");
560
+ if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, t("templates.editor.loading"));
386
561
  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}`
562
+ return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
563
+ return (0, import_vue2.h)("div", { style: styles.container }, [
564
+ (0, import_vue2.h)(
565
+ "div",
566
+ { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
567
+ [
568
+ (0, import_vue2.h)("h1", { style: styles.title }, props.type),
569
+ props.allowAddLocale && !props.locale ? (0, import_vue2.h)(
570
+ "button",
571
+ {
572
+ type: "button",
573
+ style: { ...styles.rollbackButton, marginLeft: "auto" },
574
+ onClick: () => emit("add-locale", props.type)
575
+ },
576
+ t("templates.editor.addLocale")
577
+ ) : null
578
+ ]
394
579
  ),
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", {
580
+ (0, import_vue2.h)("p", { style: styles.meta }, [
581
+ (0, import_vue2.h)("strong", props.locale ?? t("templates.defaultLocale")),
582
+ ` \xB7 v${template.value?.version ?? 1}`,
583
+ props.system ? ` \xB7 ${t("templates.editor.builtInNote")}` : ""
584
+ ]),
585
+ (0, import_vue2.h)("div", { style: styles.split }, [
586
+ (0, import_vue2.h)("div", { style: styles.column }, [
587
+ (0, import_vue2.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
588
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-subject" }, t("templates.subject")),
589
+ (0, import_vue2.h)("input", {
400
590
  id: "template-subject",
401
591
  style: styles.input,
402
592
  value: subject.value,
@@ -404,8 +594,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
404
594
  subject.value = e.target.value;
405
595
  }
406
596
  }),
407
- (0, import_vue.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
408
- (0, import_vue.h)("textarea", {
597
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-html" }, t("templates.htmlBody")),
598
+ (0, import_vue2.h)("textarea", {
409
599
  id: "template-html",
410
600
  style: styles.textarea,
411
601
  rows: 8,
@@ -414,8 +604,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
414
604
  html.value = e.target.value;
415
605
  }
416
606
  }),
417
- (0, import_vue.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
418
- (0, import_vue.h)("textarea", {
607
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-text" }, t("templates.textBody")),
608
+ (0, import_vue2.h)("textarea", {
419
609
  id: "template-text",
420
610
  style: styles.textarea,
421
611
  rows: 4,
@@ -425,32 +615,62 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
425
615
  text.value = e.target.value;
426
616
  }
427
617
  }),
428
- (0, import_vue.h)("label", { style: styles.checkboxLabel }, [
429
- (0, import_vue.h)("input", {
618
+ (0, import_vue2.h)("label", { style: styles.checkboxLabel }, [
619
+ (0, import_vue2.h)("input", {
430
620
  type: "checkbox",
431
621
  checked: active.value,
432
622
  onChange: (e) => {
433
623
  active.value = e.target.checked;
434
624
  }
435
625
  }),
436
- "Active"
626
+ t("templates.editor.active")
437
627
  ]),
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)(
628
+ saveError.value?.reason === "VERSION_CONFLICT" ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, [
629
+ `${t("templates.editor.conflict")} `,
630
+ (0, import_vue2.h)(
631
+ "button",
632
+ {
633
+ type: "button",
634
+ style: styles.rollbackButton,
635
+ onClick: () => {
636
+ saveError.value = null;
637
+ void refresh();
638
+ }
639
+ },
640
+ t("common.reload")
641
+ )
642
+ ]) : saveError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
643
+ (0, import_vue2.h)("div", { style: styles.saveRow }, [
644
+ (0, import_vue2.h)(
441
645
  "button",
442
646
  {
443
647
  type: "submit",
444
648
  disabled: isSaving.value || !dirty.value,
445
649
  style: dirty.value ? styles.button : styles.buttonDisabled
446
650
  },
447
- isSaving.value ? "Saving\u2026" : "Save"
651
+ isSaving.value ? t("common.saving") : t("common.save")
448
652
  ),
449
- !dirty.value && !isSaving.value ? (0, import_vue.h)("span", { style: styles.meta }, "No changes to save") : null
653
+ !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, t("templates.editor.noChanges")) : null,
654
+ !props.system ? (0, import_vue2.h)(
655
+ "button",
656
+ {
657
+ type: "button",
658
+ disabled: isDeleting.value,
659
+ onClick: () => void handleDelete(),
660
+ style: {
661
+ ...styles.rollbackButton,
662
+ height: "36px",
663
+ marginLeft: "auto",
664
+ color: "var(--fonderie-danger,#e00)",
665
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 40%, transparent)"
666
+ }
667
+ },
668
+ isDeleting.value ? t("common.deleting") : t("common.delete")
669
+ ) : null
450
670
  ])
451
671
  ]),
452
- (0, import_vue.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
453
- (0, import_vue.h)("textarea", {
672
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-sample" }, t("templates.editor.sampleData")),
673
+ (0, import_vue2.h)("textarea", {
454
674
  id: "template-sample",
455
675
  style: styles.textarea,
456
676
  rows: 6,
@@ -460,31 +680,31 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
460
680
  sampleJson.value = e.target.value;
461
681
  }
462
682
  }),
463
- sampleError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
683
+ sampleError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, t(sampleError.value)) : null
464
684
  ]),
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")
685
+ (0, import_vue2.h)("div", { style: styles.previewColumn }, [
686
+ (0, import_vue2.h)("div", { style: styles.previewHeader }, [
687
+ (0, import_vue2.h)("span", { style: styles.label }, t("templates.editor.preview")),
688
+ (0, import_vue2.h)("span", { style: styles.meta }, isPreviewing.value ? t("templates.editor.rendering") : t("templates.editor.live"))
469
689
  ]),
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,
690
+ previewError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
691
+ preview.value?.subject ? (0, import_vue2.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
472
692
  preview.value?.html ? (
473
693
  // sandbox= — no scripts, no same-origin. Operator-authored HTML
474
694
  // must never execute in the dashboard's origin, where the admin
475
695
  // token lives.
476
- (0, import_vue.h)("iframe", {
477
- title: "Template preview",
696
+ (0, import_vue2.h)("iframe", {
697
+ title: t("templates.editor.previewTitle"),
478
698
  style: styles.previewFrame,
479
699
  sandbox: "",
480
700
  srcdoc: preview.value.html
481
701
  })
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.")
702
+ ) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, t("templates.editor.nothingRendered"))
483
703
  ])
484
704
  ]),
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))
705
+ revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
706
+ (0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.history")),
707
+ (0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
488
708
  ]) : null
489
709
  ]);
490
710
  };
@@ -492,21 +712,53 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
492
712
  });
493
713
 
494
714
  // src/screens/TemplateListScreen.ts
715
+ var import_client3 = require("@fonderie/client");
495
716
  var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
496
- var import_vue2 = require("vue");
497
- var TemplateListScreen = (0, import_vue2.defineComponent)({
717
+ var import_vue3 = require("vue");
718
+ var builtIn = {
719
+ fontSize: "11px",
720
+ fontWeight: 500,
721
+ padding: "1px 7px",
722
+ borderRadius: "999px",
723
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
724
+ color: "var(--fonderie-text-muted,#5c5c5c)"
725
+ };
726
+ var newButton = {
727
+ display: "inline-flex",
728
+ alignItems: "center",
729
+ height: "32px",
730
+ boxSizing: "border-box",
731
+ borderRadius: "6px",
732
+ padding: "0 12px",
733
+ fontSize: "13px",
734
+ fontWeight: 500,
735
+ fontFamily: "inherit",
736
+ cursor: "pointer",
737
+ backgroundColor: "var(--fonderie-text,#171717)",
738
+ color: "var(--fonderie-surface,#fff)",
739
+ border: "1px solid var(--fonderie-text,#171717)"
740
+ };
741
+ var TemplateListScreen = (0, import_vue3.defineComponent)({
498
742
  name: "FonderieTemplateListScreen",
499
743
  props: {
500
- client: { type: Object, required: true }
744
+ client: { type: Object, required: true },
745
+ /** Shows a "New template" button (emits create-template). */
746
+ allowCreate: { type: Boolean, default: false },
747
+ /** The console language; defaults to English. */
748
+ locale: { type: String, default: void 0 }
501
749
  },
502
750
  emits: {
503
- "select-template": (_template) => true
751
+ /** The whole row — type AND locale, and whether it is built-in. */
752
+ "select-template": (_template) => true,
753
+ /** Receives the locales in use, to suggest. */
754
+ "create-template": (_context) => true
504
755
  },
505
756
  setup(props, { emit }) {
506
757
  const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
758
+ const t = (key, params) => (0, import_client3.createAdminT)(props.locale)(key, params);
507
759
  function renderRow(template) {
508
- return (0, import_vue2.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
509
- (0, import_vue2.h)(
760
+ return (0, import_vue3.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
761
+ (0, import_vue3.h)(
510
762
  "button",
511
763
  {
512
764
  type: "button",
@@ -514,29 +766,59 @@ var TemplateListScreen = (0, import_vue2.defineComponent)({
514
766
  onClick: () => emit("select-template", template)
515
767
  },
516
768
  [
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 }),
521
- template.active ? "active" : "inactive"
769
+ (0, import_vue3.h)("span", { style: styles.type }, template.type),
770
+ (0, import_vue3.h)("span", { style: styles.locale }, template.locale ?? t("templates.defaultLocale")),
771
+ template.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null,
772
+ (0, import_vue3.h)("span", { style: template.active ? styles.active : styles.inactive }, [
773
+ (0, import_vue3.h)("span", { style: styles.dot }),
774
+ template.active ? t("common.status.active") : t("common.status.inactive")
522
775
  ])
523
776
  ]
524
777
  )
525
778
  ]);
526
779
  }
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)(
780
+ return () => (0, import_vue3.h)("div", { style: styles.listContainer }, [
781
+ (0, import_vue3.h)(
782
+ "div",
783
+ {
784
+ style: {
785
+ display: "flex",
786
+ alignItems: "center",
787
+ justifyContent: "space-between",
788
+ gap: "16px"
789
+ }
790
+ },
791
+ [
792
+ (0, import_vue3.h)("h1", { style: styles.listTitle }, t("templates.list.title")),
793
+ props.allowCreate ? (0, import_vue3.h)(
794
+ "button",
795
+ {
796
+ type: "button",
797
+ style: newButton,
798
+ onClick: () => emit("create-template", {
799
+ locales: [
800
+ ...new Set(
801
+ templates.value.map((tpl) => tpl.locale).filter((l) => !!l)
802
+ )
803
+ ]
804
+ })
805
+ },
806
+ t("templates.list.newTemplate")
807
+ ) : null
808
+ ]
809
+ ),
810
+ (0, import_vue3.h)(
530
811
  "p",
531
812
  { style: styles.hint },
532
- "Every email the app sends. Open one to edit its copy and preview it live."
813
+ t("templates.list.hint")
533
814
  ),
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))
815
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, t("templates.list.loading")) : 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
816
  ]);
536
817
  }
537
818
  });
538
819
  // Annotate the CommonJS export names for ESM import in node:
539
820
  0 && (module.exports = {
821
+ TemplateCreateScreen,
540
822
  TemplateEditorScreen,
541
823
  TemplateListScreen
542
824
  });