@fonderie/vue-courier-admin-screens 0.9.0 → 0.11.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
@@ -28,6 +28,7 @@ module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/screens/TemplateCreateScreen.ts
30
30
  var import_client = require("@fonderie/client");
31
+ var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
31
32
  var import_vue = require("vue");
32
33
 
33
34
  // src/styles.ts
@@ -269,6 +270,16 @@ var styles = {
269
270
  // src/screens/TemplateCreateScreen.ts
270
271
  var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
271
272
  var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
273
+ var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
274
+ var PREVIEW_DEBOUNCE_MS = 500;
275
+ var notice = {
276
+ fontSize: "13px",
277
+ color: "var(--fonderie-text,#171717)",
278
+ background: "color-mix(in srgb, var(--fonderie-warning,#f5a623) 16%, transparent)",
279
+ borderRadius: "6px",
280
+ padding: "8px 12px",
281
+ margin: "12px 0 0"
282
+ };
272
283
  var TemplateCreateScreen = (0, import_vue.defineComponent)({
273
284
  name: "FonderieTemplateCreateScreen",
274
285
  props: {
@@ -281,6 +292,8 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
281
292
  type: { type: String, default: void 0 },
282
293
  /** Locales already in use, offered as suggestions. */
283
294
  locales: { type: Array, default: () => [] },
295
+ /** The system locale: the default version already is it, so adding it is refused. */
296
+ defaultLocale: { type: String, default: void 0 },
284
297
  /** The console language; defaults to English. */
285
298
  locale: { type: String, default: void 0 }
286
299
  },
@@ -297,15 +310,65 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
297
310
  const text = (0, import_vue.ref)("");
298
311
  const error = (0, import_vue.ref)(null);
299
312
  const busy = (0, import_vue.ref)(false);
313
+ const source = (0, import_vue.ref)(null);
314
+ const {
315
+ preview,
316
+ isPreviewing,
317
+ error: previewError,
318
+ renderPreview
319
+ } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
320
+ let variables = [];
300
321
  const tr = (key, params) => (0, import_client.createAdminT)(props.locale)(key, params);
301
322
  (0, import_vue.onMounted)(() => {
302
323
  if (!props.type) return;
303
- props.client.getTemplate(props.type, null).then(({ result }) => {
324
+ props.client.getTemplate(props.type, null).catch(() => props.client.getBuiltInTemplate(props.type, null)).then(({ result }) => {
304
325
  subject.value = result.subject ?? "";
305
326
  html.value = result.html ?? "";
306
327
  text.value = result.text;
328
+ source.value = {
329
+ subject: result.subject ?? "",
330
+ html: result.html ?? "",
331
+ text: result.text
332
+ };
307
333
  }).catch(() => void 0);
308
334
  });
335
+ let timer;
336
+ (0, import_vue.watch)([newType, targetLocale, subject, html, text], () => {
337
+ clearTimeout(timer);
338
+ const type = addingLocale ? props.type : newType.value.trim();
339
+ const l = targetLocale.value.trim();
340
+ if (!TYPE_RE.test(type) || !text.value.trim()) return;
341
+ timer = setTimeout(async () => {
342
+ const input = {
343
+ text: text.value,
344
+ ...subject.value ? { subject: subject.value } : {},
345
+ ...html.value ? { html: html.value } : {}
346
+ };
347
+ const render = () => renderPreview(
348
+ type,
349
+ { ...input, data: Object.fromEntries(variables.map((v) => [v, v])) },
350
+ LOCALE_RE.test(l) ? l : null
351
+ );
352
+ try {
353
+ const result = await render();
354
+ const used = result.variables.filter((v) => !IMPLICIT.has(v));
355
+ if (used.some((v) => !variables.includes(v))) {
356
+ variables = used;
357
+ await render();
358
+ }
359
+ } catch {
360
+ }
361
+ }, PREVIEW_DEBOUNCE_MS);
362
+ });
363
+ const untranslated = (0, import_vue.computed)(() => {
364
+ const src = source.value;
365
+ if (!src) return [];
366
+ return [
367
+ subject.value && subject.value === src.subject ? tr("templates.subject") : null,
368
+ html.value && html.value === src.html ? tr("templates.htmlBody") : null,
369
+ text.value === src.text ? tr("templates.textBody") : null
370
+ ].filter((f) => f !== null);
371
+ });
309
372
  async function submit(e) {
310
373
  e.preventDefault();
311
374
  error.value = null;
@@ -323,6 +386,10 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
323
386
  error.value = tr("templates.create.errorLocale");
324
387
  return;
325
388
  }
389
+ if (l && props.defaultLocale && l.toLowerCase() === props.defaultLocale.toLowerCase()) {
390
+ error.value = tr("templates.create.errorDefaultLocale", { locale: props.defaultLocale });
391
+ return;
392
+ }
326
393
  if (!text.value.trim()) {
327
394
  error.value = tr("templates.create.errorText");
328
395
  return;
@@ -363,7 +430,7 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
363
430
  }
364
431
  })
365
432
  ];
366
- return () => (0, import_vue.h)("div", { style: { ...styles.container, maxWidth: "760px" } }, [
433
+ return () => (0, import_vue.h)("div", { style: styles.container }, [
367
434
  (0, import_vue.h)(
368
435
  "h1",
369
436
  { style: styles.title },
@@ -374,35 +441,69 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
374
441
  { style: { ...styles.meta, lineHeight: 1.5 } },
375
442
  addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew")
376
443
  ),
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
- )
444
+ (0, import_vue.h)("div", { style: styles.split }, [
445
+ (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
446
+ ...addingLocale ? [] : field("new-template-type", tr("templates.create.type"), newType, {
447
+ placeholder: "weekly-digest"
448
+ }),
449
+ ...field(
450
+ "new-template-locale",
451
+ addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional"),
452
+ targetLocale,
453
+ { placeholder: "fr", list: "new-template-locales" }
454
+ ),
455
+ (0, import_vue.h)(
456
+ "datalist",
457
+ { id: "new-template-locales" },
458
+ [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
459
+ ),
460
+ ...field("new-template-subject", tr("templates.subject"), subject),
461
+ ...field("new-template-html", tr("templates.htmlBody"), html, {
462
+ textarea: true,
463
+ minHeight: "160px"
464
+ }),
465
+ ...field("new-template-text", tr("templates.textBody"), text, {
466
+ textarea: true,
467
+ minHeight: "100px"
468
+ }),
469
+ untranslated.value.length > 0 ? (0, import_vue.h)(
470
+ "p",
471
+ { style: notice },
472
+ tr("templates.create.untranslated", { fields: untranslated.value.join(", ") })
473
+ ) : null,
474
+ error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
475
+ (0, import_vue.h)(
476
+ "button",
477
+ {
478
+ type: "submit",
479
+ disabled: busy.value,
480
+ style: { ...styles.button, marginTop: "16px" }
481
+ },
482
+ busy.value ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew")
483
+ )
484
+ ]),
485
+ (0, import_vue.h)("div", { style: styles.previewColumn }, [
486
+ (0, import_vue.h)("div", { style: styles.previewHeader }, [
487
+ (0, import_vue.h)("span", { style: styles.label }, tr("templates.editor.preview")),
488
+ (0, import_vue.h)(
489
+ "span",
490
+ { style: styles.meta },
491
+ isPreviewing.value ? tr("templates.editor.rendering") : tr("templates.editor.live")
492
+ )
493
+ ]),
494
+ previewError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
495
+ preview.value?.subject ? (0, import_vue.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
496
+ preview.value?.html ? (
497
+ // sandbox= — no scripts, no same-origin: operator-authored HTML must
498
+ // never execute in the console's origin.
499
+ (0, import_vue.h)("iframe", {
500
+ title: tr("templates.editor.previewTitle"),
501
+ style: styles.previewFrame,
502
+ sandbox: "",
503
+ srcdoc: preview.value.html
504
+ })
505
+ ) : preview.value ? (0, import_vue.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue.h)("p", { style: styles.meta }, tr("templates.editor.nothingRendered"))
506
+ ])
406
507
  ])
407
508
  ]);
408
509
  }
@@ -410,10 +511,10 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
410
511
 
411
512
  // src/screens/TemplateEditorScreen.ts
412
513
  var import_client2 = require("@fonderie/client");
413
- var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
514
+ var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
414
515
  var import_vue2 = require("vue");
415
- var PREVIEW_DEBOUNCE_MS = 500;
416
- var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
516
+ var PREVIEW_DEBOUNCE_MS2 = 500;
517
+ var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
417
518
  var tab = {
418
519
  height: "32px",
419
520
  padding: "0 10px",
@@ -432,7 +533,8 @@ var tabSelected = {
432
533
  borderBottomColor: "var(--fonderie-text,#171717)",
433
534
  cursor: "default"
434
535
  };
435
- var tabInactive = { ...tab, opacity: 0.55, textDecoration: "line-through" };
536
+ var tabInactive = { opacity: 0.55, textDecoration: "line-through" };
537
+ var tabBuiltIn = { fontStyle: "italic" };
436
538
  var tabBar = {
437
539
  display: "flex",
438
540
  alignItems: "flex-end",
@@ -477,36 +579,53 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
477
579
  },
478
580
  setup(props, { emit }) {
479
581
  const t = (key, params) => (0, import_client2.createAdminT)(props.uiLocale)(key, params);
480
- const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
582
+ const { template, isLoading, error, refresh } = (0, import_vue_courier_admin2.useTemplate)(
481
583
  props.client,
482
584
  props.type,
483
585
  props.locale
484
586
  );
485
- const { templates, saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
486
- const siblings = (0, import_vue2.computed)(
487
- () => (0, import_client2.groupTemplatesByType)(templates.value).find((g) => g.type === props.type)?.entries ?? []
587
+ const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin2.useTemplates)(props.client);
588
+ const notSaved = (0, import_vue2.computed)(() => error.value?.status === 404);
589
+ const { builtIn: builtIn2, isLoading: builtInLoading } = (0, import_vue_courier_admin2.useBuiltInTemplate)(
590
+ props.client,
591
+ props.type,
592
+ props.locale
488
593
  );
594
+ const { catalog, refresh: refreshCatalog } = (0, import_vue_courier_admin2.useTemplateCatalog)(props.client);
595
+ const defaultLocale = (0, import_vue2.computed)(() => catalog.value?.defaultLocale ?? "en-US");
596
+ const email = (0, import_vue2.computed)(() => catalog.value?.emails.find((e) => e.type === props.type));
597
+ const languages = (0, import_vue2.computed)(
598
+ () => email.value ? (0, import_client2.templateLanguages)(email.value, defaultLocale.value) : []
599
+ );
600
+ const source = (0, import_vue2.computed)(() => template.value ?? (notSaved.value ? builtIn2.value : null));
601
+ const protectedRow = (0, import_vue2.computed)(() => !props.locale && (email.value?.system ?? props.system));
602
+ const shipsHere = (0, import_vue2.computed)(
603
+ () => props.locale ? languages.value.some((l) => l.locale === props.locale && l.builtIn) : !!email.value?.builtIn.default
604
+ );
605
+ const { resolution, resolve } = (0, import_vue_courier_admin2.useTemplateResolution)(props.client);
606
+ const probe = (0, import_vue2.ref)("");
607
+ (0, import_vue2.watch)(defaultLocale, (dl) => void resolve(props.type, props.locale ?? dl), { immediate: true });
489
608
  const isDeleting = (0, import_vue2.ref)(false);
490
- const { revisions, rollback } = (0, import_vue_courier_admin.useTemplateRevisions)(props.client, props.type, props.locale);
609
+ const { revisions, rollback } = (0, import_vue_courier_admin2.useTemplateRevisions)(props.client, props.type, props.locale);
491
610
  const isSaving = (0, import_vue2.ref)(false);
492
611
  const saveError = (0, import_vue2.ref)(null);
493
612
  const subject = (0, import_vue2.ref)("");
494
613
  const html = (0, import_vue2.ref)("");
495
614
  const text = (0, import_vue2.ref)("");
496
615
  const active = (0, import_vue2.ref)(true);
497
- (0, import_vue2.watch)(template, (tpl) => {
498
- if (!tpl) return;
499
- subject.value = tpl.subject ?? "";
500
- html.value = tpl.html ?? "";
501
- text.value = tpl.text;
502
- active.value = tpl.active;
616
+ (0, import_vue2.watch)(source, (src) => {
617
+ if (!src) return;
618
+ subject.value = src.subject ?? "";
619
+ html.value = src.html ?? "";
620
+ text.value = src.text;
621
+ active.value = template.value ? template.value.active : true;
503
622
  });
504
623
  const {
505
624
  preview,
506
625
  isPreviewing,
507
626
  error: previewError,
508
627
  renderPreview
509
- } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
628
+ } = (0, import_vue_courier_admin2.useTemplatePreview)(props.client);
510
629
  const sampleJson = (0, import_vue2.ref)("{}");
511
630
  const sampleError = (0, import_vue2.ref)(null);
512
631
  async function run() {
@@ -533,7 +652,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
533
652
  },
534
653
  props.locale
535
654
  );
536
- const missing = result.variables.filter((v) => !IMPLICIT.has(v) && !(v in data));
655
+ const missing = result.variables.filter((v) => !IMPLICIT2.has(v) && !(v in data));
537
656
  if (missing.length > 0) {
538
657
  sampleJson.value = JSON.stringify(
539
658
  { ...data, ...Object.fromEntries(missing.map((v) => [v, v])) },
@@ -544,13 +663,13 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
544
663
  }
545
664
  let previewTimer;
546
665
  (0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
547
- if (!template.value) return;
666
+ if (!source.value) return;
548
667
  clearTimeout(previewTimer);
549
- previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS);
668
+ previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS2);
550
669
  });
551
670
  const dirty = (0, import_vue2.computed)(() => {
552
671
  const tpl = template.value;
553
- return !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
672
+ return notSaved.value && !!builtIn2.value || !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
554
673
  });
555
674
  async function handleSubmit(event) {
556
675
  event.preventDefault();
@@ -562,7 +681,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
562
681
  if (html.value) input.html = html.value;
563
682
  if (template.value?.version !== void 0) input.ifVersion = template.value.version;
564
683
  await saveTemplate(props.type, input, props.locale);
565
- await refresh();
684
+ await Promise.all([refresh(), refreshCatalog()]);
566
685
  emit("saved");
567
686
  } catch (err) {
568
687
  saveError.value = err;
@@ -570,7 +689,32 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
570
689
  isSaving.value = false;
571
690
  }
572
691
  }
573
- const leave = () => !dirty.value || window.confirm(t("templates.editor.discardChanges"));
692
+ const leave = () => !dirty.value || notSaved.value && !!builtIn2.value || window.confirm(t("templates.editor.discardChanges"));
693
+ async function handleReset() {
694
+ const copy = builtIn2.value;
695
+ if (!copy || !window.confirm(
696
+ t("templates.editor.confirmReset", { locale: props.locale ?? defaultLocale.value })
697
+ ))
698
+ return;
699
+ isDeleting.value = true;
700
+ saveError.value = null;
701
+ try {
702
+ if (props.locale) {
703
+ await removeTemplate(props.type, props.locale);
704
+ } else {
705
+ const input = { text: copy.text, active: active.value };
706
+ if (copy.subject) input.subject = copy.subject;
707
+ if (copy.html) input.html = copy.html;
708
+ if (template.value?.version !== void 0) input.ifVersion = template.value.version;
709
+ await saveTemplate(props.type, input, null);
710
+ }
711
+ await Promise.all([refresh(), refreshCatalog()]);
712
+ } catch (err) {
713
+ saveError.value = err;
714
+ } finally {
715
+ isDeleting.value = false;
716
+ }
717
+ }
574
718
  function renderTabBar() {
575
719
  if (!props.localeTabs && !props.allowAddLocale) return null;
576
720
  return (0, import_vue2.h)("div", { style: tabBar }, [
@@ -580,21 +724,30 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
580
724
  (0, import_vue2.h)(
581
725
  "nav",
582
726
  { "aria-label": t("templates.editor.locales"), style: tabs },
583
- siblings.value.map((entry) => {
584
- const selected = entry.locale === (props.locale ?? null);
727
+ languages.value.map((l) => {
728
+ const selected = l.locale === (props.locale ?? null);
585
729
  return (0, import_vue2.h)(
586
730
  "button",
587
731
  {
588
- key: entry.locale ?? "",
732
+ key: l.label,
589
733
  type: "button",
590
734
  "aria-current": selected ? "true" : void 0,
591
- style: selected ? tabSelected : entry.active ? tab : tabInactive,
592
- title: entry.active ? void 0 : t("common.status.inactive"),
735
+ style: {
736
+ ...selected ? tabSelected : tab,
737
+ ...l.saved ? {} : tabBuiltIn,
738
+ ...l.active ? {} : tabInactive
739
+ },
740
+ title: l.active ? void 0 : t("common.status.inactive"),
593
741
  onClick: () => {
594
- if (!selected && leave()) emit("select-locale", entry);
742
+ if (!selected && leave())
743
+ emit("select-locale", {
744
+ type: props.type,
745
+ locale: l.locale,
746
+ system: !!email.value?.system
747
+ });
595
748
  }
596
749
  },
597
- entry.locale ?? t("templates.defaultChip")
750
+ l.label
598
751
  );
599
752
  })
600
753
  )
@@ -607,7 +760,8 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
607
760
  onClick: () => {
608
761
  if (leave())
609
762
  emit("add-locale", props.type, {
610
- locales: (0, import_client2.missingTemplateLocales)(templates.value, props.type)
763
+ locales: catalog.value ? (0, import_client2.suggestTemplateLocales)(catalog.value, props.type) : [],
764
+ defaultLocale: defaultLocale.value
611
765
  });
612
766
  }
613
767
  },
@@ -643,16 +797,19 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
643
797
  ]);
644
798
  }
645
799
  return () => {
646
- if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, t("templates.editor.loading"));
647
- if (error.value)
800
+ if (isLoading.value || notSaved.value && builtInLoading.value)
801
+ return (0, import_vue2.h)("p", { style: styles.status }, t("templates.editor.loading"));
802
+ if (error.value && !(notSaved.value && builtIn2.value))
648
803
  return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
649
804
  return (0, import_vue2.h)("div", { style: styles.container }, [
650
805
  (0, import_vue2.h)("h1", { style: styles.title }, props.type),
651
806
  renderTabBar(),
652
807
  (0, import_vue2.h)("p", { style: styles.meta }, [
653
- (0, import_vue2.h)("strong", props.locale ?? t("templates.defaultLocale")),
654
- ` \xB7 v${template.value?.version ?? 1}`,
655
- props.system ? ` \xB7 ${t("templates.editor.builtInNote")}` : ""
808
+ (0, import_vue2.h)("strong", props.locale ?? defaultLocale.value),
809
+ template.value ? ` \xB7 v${template.value.version}` : "",
810
+ template.value && protectedRow.value ? ` \xB7 ${t("templates.editor.builtInNote")}` : "",
811
+ !template.value && builtIn2.value ? ` \xB7 ${t("templates.editor.builtInCopy")}` : "",
812
+ resolution.value && resolution.value.chain.length > 1 && resolution.value.requested === (props.locale ?? defaultLocale.value) ? ` \xB7 ${t("templates.editor.chain", { chain: [...resolution.value.chain, defaultLocale.value].join(" \u2192 ") })}` : ""
656
813
  ]),
657
814
  (0, import_vue2.h)("div", { style: styles.split }, [
658
815
  (0, import_vue2.h)("div", { style: styles.column }, [
@@ -723,7 +880,16 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
723
880
  isSaving.value ? t("common.saving") : t("common.save")
724
881
  ),
725
882
  !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, t("templates.editor.noChanges")) : null,
726
- !props.system ? (0, import_vue2.h)(
883
+ template.value && shipsHere.value && builtIn2.value ? (0, import_vue2.h)(
884
+ "button",
885
+ {
886
+ type: "button",
887
+ disabled: isDeleting.value,
888
+ onClick: () => void handleReset(),
889
+ style: { ...styles.rollbackButton, height: "36px", marginLeft: "auto" }
890
+ },
891
+ t("templates.editor.resetToBuiltIn")
892
+ ) : template.value && !protectedRow.value ? (0, import_vue2.h)(
727
893
  "button",
728
894
  {
729
895
  type: "button",
@@ -782,6 +948,46 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
782
948
  ) : 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"))
783
949
  ])
784
950
  ]),
951
+ // Who receives what: any locale, answered by the same decision a send makes.
952
+ (0, import_vue2.h)("div", { style: styles.revisions }, [
953
+ (0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.whoReceives")),
954
+ (0, import_vue2.h)(
955
+ "form",
956
+ {
957
+ style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
958
+ onSubmit: (event) => {
959
+ event.preventDefault();
960
+ if (probe.value.trim()) void resolve(props.type, probe.value.trim());
961
+ }
962
+ },
963
+ [
964
+ (0, import_vue2.h)("input", {
965
+ "aria-label": t("templates.editor.whoReceives"),
966
+ style: { ...styles.input, width: "200px" },
967
+ value: probe.value,
968
+ placeholder: t("templates.editor.whoPlaceholder"),
969
+ spellcheck: false,
970
+ onInput: (e) => {
971
+ probe.value = e.target.value;
972
+ }
973
+ }),
974
+ (0, import_vue2.h)(
975
+ "button",
976
+ { type: "submit", style: { ...styles.rollbackButton, height: "36px" } },
977
+ t("templates.editor.check")
978
+ ),
979
+ resolution.value && probe.value.trim() ? (0, import_vue2.h)("span", { style: { ...styles.meta, marginBottom: "0" }, role: "status" }, [
980
+ t("templates.editor.receives", {
981
+ requested: resolution.value.requested,
982
+ sent: resolution.value.sent
983
+ }),
984
+ " ",
985
+ resolution.value.source === "saved" ? t("templates.editor.sourceSaved") : t("templates.editor.sourceBuiltIn"),
986
+ resolution.value.chain.length > 0 ? ` (${[...resolution.value.chain, resolution.value.defaultLocale].join(" \u2192 ")})` : ""
987
+ ]) : null
988
+ ]
989
+ )
990
+ ]),
785
991
  revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
786
992
  (0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.history")),
787
993
  (0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
@@ -793,15 +999,17 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
793
999
 
794
1000
  // src/screens/TemplateListScreen.ts
795
1001
  var import_client3 = require("@fonderie/client");
796
- var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
1002
+ var import_vue_courier_admin3 = require("@fonderie/vue-courier-admin");
797
1003
  var import_vue3 = require("vue");
1004
+ var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
798
1005
  var builtIn = {
799
1006
  fontSize: "11px",
800
1007
  fontWeight: 500,
801
1008
  padding: "1px 7px",
802
1009
  borderRadius: "999px",
803
1010
  border: "1px solid var(--fonderie-border,#e0e0e0)",
804
- color: "var(--fonderie-text-muted,#5c5c5c)"
1011
+ color: "var(--fonderie-text-muted,#5c5c5c)",
1012
+ flexShrink: 0
805
1013
  };
806
1014
  var chip = {
807
1015
  height: "24px",
@@ -812,23 +1020,42 @@ var chip = {
812
1020
  background: "var(--fonderie-surface,#fff)",
813
1021
  color: "var(--fonderie-text,#171717)",
814
1022
  fontSize: "12px",
815
- fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
1023
+ fontFamily: MONO,
816
1024
  cursor: "pointer"
817
1025
  };
818
- var chipInactive = { ...chip, opacity: 0.5, textDecoration: "line-through" };
1026
+ var chipBuiltIn = {
1027
+ borderStyle: "dashed",
1028
+ background: "transparent",
1029
+ color: "var(--fonderie-text-muted,#5c5c5c)"
1030
+ };
1031
+ var chipOff = { opacity: 0.5, textDecoration: "line-through" };
819
1032
  var row = {
820
1033
  ...styles.row,
821
- display: "flex",
1034
+ display: "grid",
1035
+ gridTemplateColumns: "minmax(0, 1fr) minmax(180px, 280px) 88px",
822
1036
  alignItems: "center",
823
- gap: "8px",
1037
+ gap: "16px",
824
1038
  paddingRight: "16px"
825
1039
  };
826
- var rowButton = { ...styles.rowButton, width: "auto", flex: "1", minWidth: "0" };
827
- var chips = {
828
- display: "flex",
829
- flexWrap: "wrap",
830
- justifyContent: "flex-end",
831
- gap: "6px"
1040
+ var rowButton = {
1041
+ ...styles.rowButton,
1042
+ width: "auto",
1043
+ minWidth: "0",
1044
+ justifyContent: "flex-start",
1045
+ gap: "12px"
1046
+ };
1047
+ var typeName = {
1048
+ ...styles.type,
1049
+ flex: "none",
1050
+ overflow: "hidden",
1051
+ textOverflow: "ellipsis",
1052
+ whiteSpace: "nowrap"
1053
+ };
1054
+ var chips = { display: "flex", flexWrap: "wrap", gap: "6px" };
1055
+ var legend = {
1056
+ fontSize: "12.5px",
1057
+ color: "var(--fonderie-text-muted,#5c5c5c)",
1058
+ margin: "10px 2px 0"
832
1059
  };
833
1060
  var newButton = {
834
1061
  display: "inline-flex",
@@ -856,90 +1083,97 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
856
1083
  },
857
1084
  emits: {
858
1085
  /**
859
- * The whole row — type AND locale, and whether it is built-in. One row
860
- * per email: the row opens the default locale, a locale chip that locale.
1086
+ * One row per email — built-in ones included, even when nobody saved a
1087
+ * copy. The row opens the default version; a language chip that language.
861
1088
  */
862
1089
  "select-template": (_template) => true,
863
1090
  /** Receives the locales in use, to suggest. */
864
1091
  "create-template": (_context) => true
865
1092
  },
866
1093
  setup(props, { emit }) {
867
- const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
1094
+ const { catalog, isLoading, error } = (0, import_vue_courier_admin3.useTemplateCatalog)(props.client);
868
1095
  const t = (key, params) => (0, import_client3.createAdminT)(props.locale)(key, params);
869
- function renderRow(group) {
870
- const primary = group.primary;
871
- return (0, import_vue3.h)("li", { key: group.type, style: row }, [
872
- (0, import_vue3.h)(
873
- "button",
874
- {
875
- type: "button",
876
- style: rowButton,
877
- onClick: () => emit("select-template", primary)
878
- },
879
- [
880
- (0, import_vue3.h)("span", { style: styles.type }, group.type),
881
- group.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null,
882
- (0, import_vue3.h)("span", { style: primary.active ? styles.active : styles.inactive }, [
883
- (0, import_vue3.h)("span", { style: styles.dot }),
884
- primary.active ? t("common.status.active") : t("common.status.inactive")
885
- ])
886
- ]
887
- ),
888
- // Siblings of the row button, not inside it: a button cannot hold buttons.
1096
+ const defaultLocale = () => catalog.value?.defaultLocale ?? "en-US";
1097
+ function renderRow(email) {
1098
+ const languages = (0, import_client3.templateLanguages)(email, defaultLocale());
1099
+ const primary = languages.find((l) => l.locale === null) ?? languages[0];
1100
+ const open = (l) => emit("select-template", {
1101
+ type: email.type,
1102
+ locale: l?.locale ?? null,
1103
+ system: email.system
1104
+ });
1105
+ return (0, import_vue3.h)("li", { key: email.type, style: row }, [
1106
+ (0, import_vue3.h)("button", { type: "button", style: rowButton, onClick: () => open(primary) }, [
1107
+ (0, import_vue3.h)("span", { style: typeName }, email.type),
1108
+ email.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null
1109
+ ]),
1110
+ // Siblings of the row button: a button cannot hold buttons. Each chip
1111
+ // names its email and language for screen readers.
889
1112
  (0, import_vue3.h)(
890
1113
  "span",
891
1114
  { style: chips },
892
- group.entries.map((entry) => {
893
- const name = entry.locale ?? t("templates.defaultChip");
894
- return (0, import_vue3.h)(
1115
+ languages.map(
1116
+ (l) => (0, import_vue3.h)(
895
1117
  "button",
896
1118
  {
897
- key: entry.locale ?? "",
1119
+ key: l.label,
898
1120
  type: "button",
899
- style: entry.active ? chip : chipInactive,
900
- "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
901
- title: entry.active ? void 0 : t("common.status.inactive"),
902
- onClick: () => emit("select-template", entry)
1121
+ style: { ...chip, ...l.saved ? {} : chipBuiltIn, ...l.active ? {} : chipOff },
1122
+ "aria-label": t("templates.list.openLocale", { type: email.type, locale: l.label }),
1123
+ title: l.active ? void 0 : t("common.status.inactive"),
1124
+ onClick: () => open(l)
903
1125
  },
904
- name
905
- );
906
- })
907
- )
1126
+ l.label
1127
+ )
1128
+ )
1129
+ ),
1130
+ (0, import_vue3.h)("span", { style: primary?.active === false ? styles.inactive : styles.active }, [
1131
+ (0, import_vue3.h)("span", { style: styles.dot }),
1132
+ primary?.active === false ? t("common.status.inactive") : t("common.status.active")
1133
+ ])
908
1134
  ]);
909
1135
  }
910
- return () => (0, import_vue3.h)("div", { style: styles.listContainer }, [
911
- (0, import_vue3.h)(
912
- "div",
913
- {
914
- style: {
915
- display: "flex",
916
- alignItems: "center",
917
- justifyContent: "space-between",
918
- gap: "16px"
919
- }
920
- },
921
- [
922
- (0, import_vue3.h)("h1", { style: styles.listTitle }, t("templates.list.title")),
923
- props.allowCreate ? (0, import_vue3.h)(
924
- "button",
925
- {
926
- type: "button",
927
- style: newButton,
928
- onClick: () => emit("create-template", {
929
- locales: [
930
- ...new Set(
931
- templates.value.map((tpl) => tpl.locale).filter((l) => !!l)
932
- )
933
- ]
934
- })
935
- },
936
- t("templates.list.newTemplate")
937
- ) : null
1136
+ return () => {
1137
+ const emails = catalog.value?.emails ?? [];
1138
+ return (0, import_vue3.h)("div", { style: styles.listContainer }, [
1139
+ (0, import_vue3.h)(
1140
+ "div",
1141
+ {
1142
+ style: {
1143
+ display: "flex",
1144
+ alignItems: "center",
1145
+ justifyContent: "space-between",
1146
+ gap: "16px"
1147
+ }
1148
+ },
1149
+ [
1150
+ (0, import_vue3.h)("h1", { style: styles.listTitle }, t("templates.list.title")),
1151
+ props.allowCreate ? (0, import_vue3.h)(
1152
+ "button",
1153
+ {
1154
+ type: "button",
1155
+ style: newButton,
1156
+ onClick: () => emit("create-template", {
1157
+ locales: [
1158
+ ...new Set(
1159
+ emails.flatMap(
1160
+ (e) => (0, import_client3.templateLanguages)(e, defaultLocale()).filter((l) => l.locale !== null).map((l) => l.label)
1161
+ )
1162
+ )
1163
+ ].sort()
1164
+ })
1165
+ },
1166
+ t("templates.list.newTemplate")
1167
+ ) : null
1168
+ ]
1169
+ ),
1170
+ (0, import_vue3.h)("p", { style: styles.hint }, t("templates.list.hint")),
1171
+ 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) : [
1172
+ (0, import_vue3.h)("ul", { style: styles.list }, emails.map(renderRow)),
1173
+ (0, import_vue3.h)("p", { style: legend }, t("templates.list.legend"))
938
1174
  ]
939
- ),
940
- (0, import_vue3.h)("p", { style: styles.hint }, t("templates.list.hint")),
941
- 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 }, (0, import_client3.groupTemplatesByType)(templates.value).map(renderRow))
942
- ]);
1175
+ ]);
1176
+ };
943
1177
  }
944
1178
  });
945
1179
  // Annotate the CommonJS export names for ESM import in node: