@fonderie/vue-courier-admin-screens 0.8.0 → 0.10.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: {
@@ -297,6 +308,14 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
297
308
  const text = (0, import_vue.ref)("");
298
309
  const error = (0, import_vue.ref)(null);
299
310
  const busy = (0, import_vue.ref)(false);
311
+ const source = (0, import_vue.ref)(null);
312
+ const {
313
+ preview,
314
+ isPreviewing,
315
+ error: previewError,
316
+ renderPreview
317
+ } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
318
+ let variables = [];
300
319
  const tr = (key, params) => (0, import_client.createAdminT)(props.locale)(key, params);
301
320
  (0, import_vue.onMounted)(() => {
302
321
  if (!props.type) return;
@@ -304,8 +323,50 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
304
323
  subject.value = result.subject ?? "";
305
324
  html.value = result.html ?? "";
306
325
  text.value = result.text;
326
+ source.value = {
327
+ subject: result.subject ?? "",
328
+ html: result.html ?? "",
329
+ text: result.text
330
+ };
307
331
  }).catch(() => void 0);
308
332
  });
333
+ let timer;
334
+ (0, import_vue.watch)([newType, targetLocale, subject, html, text], () => {
335
+ clearTimeout(timer);
336
+ const type = addingLocale ? props.type : newType.value.trim();
337
+ const l = targetLocale.value.trim();
338
+ if (!TYPE_RE.test(type) || !text.value.trim()) return;
339
+ timer = setTimeout(async () => {
340
+ const input = {
341
+ text: text.value,
342
+ ...subject.value ? { subject: subject.value } : {},
343
+ ...html.value ? { html: html.value } : {}
344
+ };
345
+ const render = () => renderPreview(
346
+ type,
347
+ { ...input, data: Object.fromEntries(variables.map((v) => [v, v])) },
348
+ LOCALE_RE.test(l) ? l : null
349
+ );
350
+ try {
351
+ const result = await render();
352
+ const used = result.variables.filter((v) => !IMPLICIT.has(v));
353
+ if (used.some((v) => !variables.includes(v))) {
354
+ variables = used;
355
+ await render();
356
+ }
357
+ } catch {
358
+ }
359
+ }, PREVIEW_DEBOUNCE_MS);
360
+ });
361
+ const untranslated = (0, import_vue.computed)(() => {
362
+ const src = source.value;
363
+ if (!src) return [];
364
+ return [
365
+ subject.value && subject.value === src.subject ? tr("templates.subject") : null,
366
+ html.value && html.value === src.html ? tr("templates.htmlBody") : null,
367
+ text.value === src.text ? tr("templates.textBody") : null
368
+ ].filter((f) => f !== null);
369
+ });
309
370
  async function submit(e) {
310
371
  e.preventDefault();
311
372
  error.value = null;
@@ -363,7 +424,7 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
363
424
  }
364
425
  })
365
426
  ];
366
- return () => (0, import_vue.h)("div", { style: { ...styles.container, maxWidth: "760px" } }, [
427
+ return () => (0, import_vue.h)("div", { style: styles.container }, [
367
428
  (0, import_vue.h)(
368
429
  "h1",
369
430
  { style: styles.title },
@@ -374,35 +435,69 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
374
435
  { style: { ...styles.meta, lineHeight: 1.5 } },
375
436
  addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew")
376
437
  ),
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
- )
438
+ (0, import_vue.h)("div", { style: styles.split }, [
439
+ (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
440
+ ...addingLocale ? [] : field("new-template-type", tr("templates.create.type"), newType, {
441
+ placeholder: "weekly-digest"
442
+ }),
443
+ ...field(
444
+ "new-template-locale",
445
+ addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional"),
446
+ targetLocale,
447
+ { placeholder: "fr", list: "new-template-locales" }
448
+ ),
449
+ (0, import_vue.h)(
450
+ "datalist",
451
+ { id: "new-template-locales" },
452
+ [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
453
+ ),
454
+ ...field("new-template-subject", tr("templates.subject"), subject),
455
+ ...field("new-template-html", tr("templates.htmlBody"), html, {
456
+ textarea: true,
457
+ minHeight: "160px"
458
+ }),
459
+ ...field("new-template-text", tr("templates.textBody"), text, {
460
+ textarea: true,
461
+ minHeight: "100px"
462
+ }),
463
+ untranslated.value.length > 0 ? (0, import_vue.h)(
464
+ "p",
465
+ { style: notice },
466
+ tr("templates.create.untranslated", { fields: untranslated.value.join(", ") })
467
+ ) : null,
468
+ error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
469
+ (0, import_vue.h)(
470
+ "button",
471
+ {
472
+ type: "submit",
473
+ disabled: busy.value,
474
+ style: { ...styles.button, marginTop: "16px" }
475
+ },
476
+ busy.value ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew")
477
+ )
478
+ ]),
479
+ (0, import_vue.h)("div", { style: styles.previewColumn }, [
480
+ (0, import_vue.h)("div", { style: styles.previewHeader }, [
481
+ (0, import_vue.h)("span", { style: styles.label }, tr("templates.editor.preview")),
482
+ (0, import_vue.h)(
483
+ "span",
484
+ { style: styles.meta },
485
+ isPreviewing.value ? tr("templates.editor.rendering") : tr("templates.editor.live")
486
+ )
487
+ ]),
488
+ previewError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
489
+ preview.value?.subject ? (0, import_vue.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
490
+ preview.value?.html ? (
491
+ // sandbox= — no scripts, no same-origin: operator-authored HTML must
492
+ // never execute in the console's origin.
493
+ (0, import_vue.h)("iframe", {
494
+ title: tr("templates.editor.previewTitle"),
495
+ style: styles.previewFrame,
496
+ sandbox: "",
497
+ srcdoc: preview.value.html
498
+ })
499
+ ) : 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"))
500
+ ])
406
501
  ])
407
502
  ]);
408
503
  }
@@ -410,10 +505,38 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
410
505
 
411
506
  // src/screens/TemplateEditorScreen.ts
412
507
  var import_client2 = require("@fonderie/client");
413
- var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
508
+ var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
414
509
  var import_vue2 = require("vue");
415
- var PREVIEW_DEBOUNCE_MS = 500;
416
- var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
510
+ var PREVIEW_DEBOUNCE_MS2 = 500;
511
+ var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
512
+ var tab = {
513
+ height: "32px",
514
+ padding: "0 10px",
515
+ background: "none",
516
+ border: "none",
517
+ borderBottom: "2px solid transparent",
518
+ color: "var(--fonderie-text-muted,#5c5c5c)",
519
+ fontSize: "13px",
520
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
521
+ cursor: "pointer"
522
+ };
523
+ var tabSelected = {
524
+ ...tab,
525
+ color: "var(--fonderie-text,#171717)",
526
+ fontWeight: 600,
527
+ borderBottomColor: "var(--fonderie-text,#171717)",
528
+ cursor: "default"
529
+ };
530
+ var tabInactive = { ...tab, opacity: 0.55, textDecoration: "line-through" };
531
+ var tabBar = {
532
+ display: "flex",
533
+ alignItems: "flex-end",
534
+ gap: "12px",
535
+ borderBottom: "1px solid var(--fonderie-border,#e0e0e0)",
536
+ margin: "4px 0 12px",
537
+ paddingBottom: "6px"
538
+ };
539
+ var tabs = { display: "flex", flexWrap: "wrap", gap: "4px", marginBottom: "-7px" };
417
540
  var TemplateEditorScreen = (0, import_vue2.defineComponent)({
418
541
  name: "FonderieTemplateEditorScreen",
419
542
  props: {
@@ -425,8 +548,13 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
425
548
  * be edited and rolled back, never deleted — Delete is not offered.
426
549
  */
427
550
  system: { type: Boolean, default: false },
428
- /** Shows "+ Add locale" on the default locale (emits add-locale). */
551
+ /** Shows "+ Add locale" (emits add-locale). */
429
552
  allowAddLocale: { type: Boolean, default: false },
553
+ /**
554
+ * Shows a tab per locale this email exists in (emits select-locale).
555
+ * Unsaved edits are confirmed before switching away.
556
+ */
557
+ localeTabs: { type: Boolean, default: false },
430
558
  /** The console language (`locale` is the template's); defaults to English. */
431
559
  uiLocale: { type: String, default: void 0 }
432
560
  },
@@ -434,19 +562,27 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
434
562
  saved: () => true,
435
563
  /** The template was deleted and no longer exists. */
436
564
  deleted: () => true,
437
- /** Create a translation of this template. */
438
- "add-locale": (_type) => true
565
+ /**
566
+ * Create a translation of this template. `locales` are the ones the app
567
+ * uses elsewhere that this email lacks — suggest them first.
568
+ */
569
+ "add-locale": (_type, _context) => true,
570
+ /** Open another locale of this email. */
571
+ "select-locale": (_template) => true
439
572
  },
440
573
  setup(props, { emit }) {
441
574
  const t = (key, params) => (0, import_client2.createAdminT)(props.uiLocale)(key, params);
442
- const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
575
+ const { template, isLoading, error, refresh } = (0, import_vue_courier_admin2.useTemplate)(
443
576
  props.client,
444
577
  props.type,
445
578
  props.locale
446
579
  );
447
- const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
580
+ const { templates, saveTemplate, removeTemplate } = (0, import_vue_courier_admin2.useTemplates)(props.client);
581
+ const siblings = (0, import_vue2.computed)(
582
+ () => (0, import_client2.groupTemplatesByType)(templates.value).find((g) => g.type === props.type)?.entries ?? []
583
+ );
448
584
  const isDeleting = (0, import_vue2.ref)(false);
449
- const { revisions, rollback } = (0, import_vue_courier_admin.useTemplateRevisions)(props.client, props.type, props.locale);
585
+ const { revisions, rollback } = (0, import_vue_courier_admin2.useTemplateRevisions)(props.client, props.type, props.locale);
450
586
  const isSaving = (0, import_vue2.ref)(false);
451
587
  const saveError = (0, import_vue2.ref)(null);
452
588
  const subject = (0, import_vue2.ref)("");
@@ -465,7 +601,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
465
601
  isPreviewing,
466
602
  error: previewError,
467
603
  renderPreview
468
- } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
604
+ } = (0, import_vue_courier_admin2.useTemplatePreview)(props.client);
469
605
  const sampleJson = (0, import_vue2.ref)("{}");
470
606
  const sampleError = (0, import_vue2.ref)(null);
471
607
  async function run() {
@@ -492,7 +628,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
492
628
  },
493
629
  props.locale
494
630
  );
495
- const missing = result.variables.filter((v) => !IMPLICIT.has(v) && !(v in data));
631
+ const missing = result.variables.filter((v) => !IMPLICIT2.has(v) && !(v in data));
496
632
  if (missing.length > 0) {
497
633
  sampleJson.value = JSON.stringify(
498
634
  { ...data, ...Object.fromEntries(missing.map((v) => [v, v])) },
@@ -505,7 +641,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
505
641
  (0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
506
642
  if (!template.value) return;
507
643
  clearTimeout(previewTimer);
508
- previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS);
644
+ previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS2);
509
645
  });
510
646
  const dirty = (0, import_vue2.computed)(() => {
511
647
  const tpl = template.value;
@@ -529,6 +665,51 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
529
665
  isSaving.value = false;
530
666
  }
531
667
  }
668
+ const leave = () => !dirty.value || window.confirm(t("templates.editor.discardChanges"));
669
+ function renderTabBar() {
670
+ if (!props.localeTabs && !props.allowAddLocale) return null;
671
+ return (0, import_vue2.h)("div", { style: tabBar }, [
672
+ props.localeTabs ? (
673
+ // Each tab opens another stored row, so this is navigation, not an
674
+ // ARIA tablist: plain buttons, the open one marked aria-current.
675
+ (0, import_vue2.h)(
676
+ "nav",
677
+ { "aria-label": t("templates.editor.locales"), style: tabs },
678
+ siblings.value.map((entry) => {
679
+ const selected = entry.locale === (props.locale ?? null);
680
+ return (0, import_vue2.h)(
681
+ "button",
682
+ {
683
+ key: entry.locale ?? "",
684
+ type: "button",
685
+ "aria-current": selected ? "true" : void 0,
686
+ style: selected ? tabSelected : entry.active ? tab : tabInactive,
687
+ title: entry.active ? void 0 : t("common.status.inactive"),
688
+ onClick: () => {
689
+ if (!selected && leave()) emit("select-locale", entry);
690
+ }
691
+ },
692
+ entry.locale ?? t("templates.defaultChip")
693
+ );
694
+ })
695
+ )
696
+ ) : null,
697
+ props.allowAddLocale ? (0, import_vue2.h)(
698
+ "button",
699
+ {
700
+ type: "button",
701
+ style: { ...styles.rollbackButton, marginLeft: "auto" },
702
+ onClick: () => {
703
+ if (leave())
704
+ emit("add-locale", props.type, {
705
+ locales: (0, import_client2.missingTemplateLocales)(templates.value, props.type)
706
+ });
707
+ }
708
+ },
709
+ t("templates.editor.addLocale")
710
+ ) : null
711
+ ]);
712
+ }
532
713
  async function handleDelete() {
533
714
  const message = props.locale ? t("templates.editor.confirmDeleteLocale", { locale: props.locale, type: props.type }) : t("templates.editor.confirmDelete", { type: props.type });
534
715
  if (!window.confirm(message)) return;
@@ -561,22 +742,8 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
561
742
  if (error.value)
562
743
  return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
563
744
  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
- ]
579
- ),
745
+ (0, import_vue2.h)("h1", { style: styles.title }, props.type),
746
+ renderTabBar(),
580
747
  (0, import_vue2.h)("p", { style: styles.meta }, [
581
748
  (0, import_vue2.h)("strong", props.locale ?? t("templates.defaultLocale")),
582
749
  ` \xB7 v${template.value?.version ?? 1}`,
@@ -669,7 +836,11 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
669
836
  ) : null
670
837
  ])
671
838
  ]),
672
- (0, import_vue2.h)("label", { style: styles.label, for: "template-sample" }, t("templates.editor.sampleData")),
839
+ (0, import_vue2.h)(
840
+ "label",
841
+ { style: styles.label, for: "template-sample" },
842
+ t("templates.editor.sampleData")
843
+ ),
673
844
  (0, import_vue2.h)("textarea", {
674
845
  id: "template-sample",
675
846
  style: styles.textarea,
@@ -685,7 +856,11 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
685
856
  (0, import_vue2.h)("div", { style: styles.previewColumn }, [
686
857
  (0, import_vue2.h)("div", { style: styles.previewHeader }, [
687
858
  (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"))
859
+ (0, import_vue2.h)(
860
+ "span",
861
+ { style: styles.meta },
862
+ isPreviewing.value ? t("templates.editor.rendering") : t("templates.editor.live")
863
+ )
689
864
  ]),
690
865
  previewError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
691
866
  preview.value?.subject ? (0, import_vue2.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
@@ -713,7 +888,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
713
888
 
714
889
  // src/screens/TemplateListScreen.ts
715
890
  var import_client3 = require("@fonderie/client");
716
- var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
891
+ var import_vue_courier_admin3 = require("@fonderie/vue-courier-admin");
717
892
  var import_vue3 = require("vue");
718
893
  var builtIn = {
719
894
  fontSize: "11px",
@@ -723,6 +898,33 @@ var builtIn = {
723
898
  border: "1px solid var(--fonderie-border,#e0e0e0)",
724
899
  color: "var(--fonderie-text-muted,#5c5c5c)"
725
900
  };
901
+ var chip = {
902
+ height: "24px",
903
+ boxSizing: "border-box",
904
+ padding: "0 8px",
905
+ borderRadius: "6px",
906
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
907
+ background: "var(--fonderie-surface,#fff)",
908
+ color: "var(--fonderie-text,#171717)",
909
+ fontSize: "12px",
910
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
911
+ cursor: "pointer"
912
+ };
913
+ var chipInactive = { ...chip, opacity: 0.5, textDecoration: "line-through" };
914
+ var row = {
915
+ ...styles.row,
916
+ display: "flex",
917
+ alignItems: "center",
918
+ gap: "8px",
919
+ paddingRight: "16px"
920
+ };
921
+ var rowButton = { ...styles.rowButton, width: "auto", flex: "1", minWidth: "0" };
922
+ var chips = {
923
+ display: "flex",
924
+ flexWrap: "wrap",
925
+ justifyContent: "flex-end",
926
+ gap: "6px"
927
+ };
726
928
  var newButton = {
727
929
  display: "inline-flex",
728
930
  alignItems: "center",
@@ -748,32 +950,55 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
748
950
  locale: { type: String, default: void 0 }
749
951
  },
750
952
  emits: {
751
- /** The whole row — type AND locale, and whether it is built-in. */
953
+ /**
954
+ * The whole row — type AND locale, and whether it is built-in. One row
955
+ * per email: the row opens the default locale, a locale chip that locale.
956
+ */
752
957
  "select-template": (_template) => true,
753
958
  /** Receives the locales in use, to suggest. */
754
959
  "create-template": (_context) => true
755
960
  },
756
961
  setup(props, { emit }) {
757
- const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
962
+ const { templates, isLoading, error } = (0, import_vue_courier_admin3.useTemplates)(props.client);
758
963
  const t = (key, params) => (0, import_client3.createAdminT)(props.locale)(key, params);
759
- function renderRow(template) {
760
- return (0, import_vue3.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
964
+ function renderRow(group) {
965
+ const primary = group.primary;
966
+ return (0, import_vue3.h)("li", { key: group.type, style: row }, [
761
967
  (0, import_vue3.h)(
762
968
  "button",
763
969
  {
764
970
  type: "button",
765
- style: styles.rowButton,
766
- onClick: () => emit("select-template", template)
971
+ style: rowButton,
972
+ onClick: () => emit("select-template", primary)
767
973
  },
768
974
  [
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 }, [
975
+ (0, import_vue3.h)("span", { style: styles.type }, group.type),
976
+ group.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null,
977
+ (0, import_vue3.h)("span", { style: primary.active ? styles.active : styles.inactive }, [
773
978
  (0, import_vue3.h)("span", { style: styles.dot }),
774
- template.active ? t("common.status.active") : t("common.status.inactive")
979
+ primary.active ? t("common.status.active") : t("common.status.inactive")
775
980
  ])
776
981
  ]
982
+ ),
983
+ // Siblings of the row button, not inside it: a button cannot hold buttons.
984
+ (0, import_vue3.h)(
985
+ "span",
986
+ { style: chips },
987
+ group.entries.map((entry) => {
988
+ const name = entry.locale ?? t("templates.defaultChip");
989
+ return (0, import_vue3.h)(
990
+ "button",
991
+ {
992
+ key: entry.locale ?? "",
993
+ type: "button",
994
+ style: entry.active ? chip : chipInactive,
995
+ "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
996
+ title: entry.active ? void 0 : t("common.status.inactive"),
997
+ onClick: () => emit("select-template", entry)
998
+ },
999
+ name
1000
+ );
1001
+ })
777
1002
  )
778
1003
  ]);
779
1004
  }
@@ -807,12 +1032,8 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
807
1032
  ) : null
808
1033
  ]
809
1034
  ),
810
- (0, import_vue3.h)(
811
- "p",
812
- { style: styles.hint },
813
- t("templates.list.hint")
814
- ),
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))
1035
+ (0, import_vue3.h)("p", { style: styles.hint }, t("templates.list.hint")),
1036
+ 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))
816
1037
  ]);
817
1038
  }
818
1039
  });