@fonderie/react-courier-admin-screens 0.10.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
@@ -46,8 +46,16 @@ function TemplateEditorScreen({
46
46
  }) {
47
47
  const t = (0, import_react.useMemo)(() => (0, import_client.createAdminT)(uiLocale), [uiLocale]);
48
48
  const { template, isLoading, error, refresh } = (0, import_react_courier_admin.useTemplate)(client, type, locale);
49
- const { templates, saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
50
- const siblings = (0, import_client.groupTemplatesByType)(templates).find((g) => g.type === type)?.entries ?? [];
49
+ const notSaved = error?.status === 404;
50
+ const { builtIn, isLoading: builtInLoading } = (0, import_react_courier_admin.useBuiltInTemplate)(client, type, locale);
51
+ const { catalog, refresh: refreshCatalog } = (0, import_react_courier_admin.useTemplateCatalog)(client);
52
+ const defaultLocale = catalog?.defaultLocale ?? "en-US";
53
+ const email = catalog?.emails.find((e) => e.type === type);
54
+ const languages = email ? (0, import_client.templateLanguages)(email, defaultLocale) : [];
55
+ const source = template ?? (notSaved ? builtIn : null);
56
+ const { resolution, resolve } = (0, import_react_courier_admin.useTemplateResolution)(client);
57
+ const [probe, setProbe] = (0, import_react.useState)("");
58
+ const { saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
51
59
  const [isDeleting, setIsDeleting] = (0, import_react.useState)(false);
52
60
  const { revisions, rollback } = (0, import_react_courier_admin.useTemplateRevisions)(client, type, locale);
53
61
  const [isSaving, setIsSaving] = (0, import_react.useState)(false);
@@ -60,12 +68,15 @@ function TemplateEditorScreen({
60
68
  const [sampleJson, setSampleJson] = (0, import_react.useState)("{}");
61
69
  const [sampleError, setSampleError] = (0, import_react.useState)(null);
62
70
  (0, import_react.useEffect)(() => {
63
- if (!template) return;
64
- setSubject(template.subject ?? "");
65
- setHtml(template.html ?? "");
66
- setText(template.text);
67
- setActive(template.active);
68
- }, [template]);
71
+ if (!source) return;
72
+ setSubject(source.subject ?? "");
73
+ setHtml(source.html ?? "");
74
+ setText(source.text);
75
+ setActive(template ? template.active : true);
76
+ }, [source, template]);
77
+ (0, import_react.useEffect)(() => {
78
+ void resolve(type, locale ?? defaultLocale);
79
+ }, [resolve, type, locale, defaultLocale]);
69
80
  const run = (0, import_react.useCallback)(async () => {
70
81
  let data = {};
71
82
  try {
@@ -93,13 +104,15 @@ function TemplateEditorScreen({
93
104
  }, [renderPreview, type, text, subject, html, locale, sampleJson, t]);
94
105
  const runRef = (0, import_react.useRef)(run);
95
106
  runRef.current = run;
96
- const loadedType = template ? `${type}:${locale ?? ""}` : null;
107
+ const loadedType = source ? `${type}:${locale ?? ""}` : null;
97
108
  (0, import_react.useEffect)(() => {
98
109
  if (!loadedType) return;
99
110
  const timer = setTimeout(() => void runRef.current(), PREVIEW_DEBOUNCE_MS);
100
111
  return () => clearTimeout(timer);
101
112
  }, [loadedType, subject, html, text, sampleJson]);
102
- const dirty = !!template && (subject !== (template.subject ?? "") || html !== (template.html ?? "") || text !== template.text || active !== template.active);
113
+ const dirty = notSaved && !!builtIn || !!template && (subject !== (template.subject ?? "") || html !== (template.html ?? "") || text !== template.text || active !== template.active);
114
+ const protectedRow = !locale && (email?.system ?? system);
115
+ const shipsHere = locale ? languages.some((l) => l.locale === locale && l.builtIn) : !!email?.builtIn.default;
103
116
  const handleSubmit = async (event) => {
104
117
  event.preventDefault();
105
118
  setIsSaving(true);
@@ -110,7 +123,7 @@ function TemplateEditorScreen({
110
123
  if (html) input.html = html;
111
124
  if (template?.version !== void 0) input.ifVersion = template.version;
112
125
  await saveTemplate(type, input, locale);
113
- await refresh();
126
+ await Promise.all([refresh(), refreshCatalog()]);
114
127
  onSaved?.();
115
128
  } catch (err) {
116
129
  setSaveError(err);
@@ -118,9 +131,32 @@ function TemplateEditorScreen({
118
131
  setIsSaving(false);
119
132
  }
120
133
  };
121
- const switchTo = (entry) => {
122
- if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
123
- onSelectLocale?.(entry);
134
+ const switchTo = (next) => {
135
+ if (dirty && !(notSaved && !!builtIn) && !window.confirm(t("templates.editor.discardChanges")))
136
+ return;
137
+ onSelectLocale?.(next);
138
+ };
139
+ const handleReset = async () => {
140
+ if (!builtIn || !window.confirm(t("templates.editor.confirmReset", { locale: locale ?? defaultLocale })))
141
+ return;
142
+ setIsDeleting(true);
143
+ setSaveError(null);
144
+ try {
145
+ if (locale) {
146
+ await removeTemplate(type, locale);
147
+ } else {
148
+ const input = { text: builtIn.text, active };
149
+ if (builtIn.subject) input.subject = builtIn.subject;
150
+ if (builtIn.html) input.html = builtIn.html;
151
+ if (template?.version !== void 0) input.ifVersion = template.version;
152
+ await saveTemplate(type, input, null);
153
+ }
154
+ await Promise.all([refresh(), refreshCatalog()]);
155
+ } catch (err) {
156
+ setSaveError(err);
157
+ } finally {
158
+ setIsDeleting(false);
159
+ }
124
160
  };
125
161
  const handleDelete = async () => {
126
162
  const message = locale ? t("templates.editor.confirmDeleteLocale", { type, locale }) : t("templates.editor.confirmDelete", { type });
@@ -136,8 +172,9 @@ function TemplateEditorScreen({
136
172
  setIsDeleting(false);
137
173
  }
138
174
  };
139
- if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: t("templates.editor.loading") });
140
- if (error)
175
+ if (isLoading || notSaved && builtInLoading)
176
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: t("templates.editor.loading") });
177
+ if (error && !(notSaved && builtIn))
141
178
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: error.explanation });
142
179
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.container, children: [
143
180
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
@@ -145,19 +182,23 @@ function TemplateEditorScreen({
145
182
  onSelectLocale ? (
146
183
  // Each tab opens another stored row, so this is navigation, not an
147
184
  // ARIA tablist: plain buttons, the open one marked aria-current.
148
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children: siblings.map((entry) => {
149
- const selected = entry.locale === (locale ?? null);
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children: languages.map((l) => {
186
+ const selected = l.locale === (locale ?? null);
150
187
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
151
188
  "button",
152
189
  {
153
190
  type: "button",
154
191
  "aria-current": selected ? "true" : void 0,
155
- onClick: () => selected ? void 0 : switchTo(entry),
156
- style: selected ? styles.tabSelected : entry.active ? styles.tab : styles.tabInactive,
157
- title: entry.active ? void 0 : t("common.status.inactive"),
158
- children: entry.locale ?? t("templates.defaultChip")
192
+ onClick: () => selected ? void 0 : switchTo({ type, locale: l.locale, system: !!email?.system }),
193
+ style: {
194
+ ...selected ? styles.tabSelected : styles.tab,
195
+ ...l.saved ? {} : styles.tabBuiltIn,
196
+ ...l.active ? {} : styles.tabInactive
197
+ },
198
+ title: l.active ? void 0 : t("common.status.inactive"),
199
+ children: l.label
159
200
  },
160
- entry.locale ?? ""
201
+ l.label
161
202
  );
162
203
  }) })
163
204
  ) : null,
@@ -168,17 +209,21 @@ function TemplateEditorScreen({
168
209
  style: { ...styles.rollbackButton, marginLeft: "auto" },
169
210
  onClick: () => {
170
211
  if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
171
- onAddLocale(type, { locales: (0, import_client.missingTemplateLocales)(templates, type) });
212
+ onAddLocale(type, {
213
+ locales: catalog ? (0, import_client.suggestTemplateLocales)(catalog, type) : [],
214
+ defaultLocale
215
+ });
172
216
  },
173
217
  children: t("templates.editor.addLocale")
174
218
  }
175
219
  ) : null
176
220
  ] }) : null,
177
221
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.meta, children: [
178
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? t("templates.defaultLocale") }),
179
- " \xB7 v",
180
- template?.version ?? 1,
181
- system ? ` \xB7 ${t("templates.editor.builtInNote")}` : ""
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? defaultLocale }),
223
+ template ? ` \xB7 v${template.version}` : "",
224
+ template && protectedRow ? ` \xB7 ${t("templates.editor.builtInNote")}` : "",
225
+ !template && builtIn ? ` \xB7 ${t("templates.editor.builtInCopy")}` : "",
226
+ resolution && resolution.chain.length > 1 && resolution.requested === (locale ?? defaultLocale) ? ` \xB7 ${t("templates.editor.chain", { chain: [...resolution.chain, defaultLocale].join(" \u2192 ") })}` : ""
182
227
  ] }),
183
228
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.split, children: [
184
229
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.column, children: [
@@ -254,7 +299,16 @@ function TemplateEditorScreen({
254
299
  }
255
300
  ),
256
301
  !dirty && !isSaving && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: t("templates.editor.noChanges") }),
257
- !system ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
302
+ template && shipsHere && builtIn ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
303
+ "button",
304
+ {
305
+ type: "button",
306
+ disabled: isDeleting,
307
+ onClick: () => void handleReset(),
308
+ style: { ...styles.rollbackButton, height: 36, marginLeft: "auto" },
309
+ children: t("templates.editor.resetToBuiltIn")
310
+ }
311
+ ) : template && !protectedRow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
258
312
  "button",
259
313
  {
260
314
  type: "button",
@@ -309,6 +363,43 @@ function TemplateEditorScreen({
309
363
  ) : preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { style: styles.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.meta, children: t("templates.editor.nothingRendered") })
310
364
  ] })
311
365
  ] }),
366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.revisions, children: [
367
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: t("templates.editor.whoReceives") }),
368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
369
+ "form",
370
+ {
371
+ style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
372
+ onSubmit: (event) => {
373
+ event.preventDefault();
374
+ if (probe.trim()) void resolve(type, probe.trim());
375
+ },
376
+ children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
378
+ "input",
379
+ {
380
+ "aria-label": t("templates.editor.whoReceives"),
381
+ style: { ...styles.input, width: 200 },
382
+ value: probe,
383
+ placeholder: t("templates.editor.whoPlaceholder"),
384
+ spellCheck: false,
385
+ onChange: (event) => setProbe(event.target.value)
386
+ }
387
+ ),
388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", style: { ...styles.rollbackButton, height: 36 }, children: t("templates.editor.check") }),
389
+ resolution && probe.trim() ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...styles.meta, marginBottom: 0 }, role: "status", children: [
390
+ t("templates.editor.receives", {
391
+ requested: resolution.requested,
392
+ sent: resolution.sent
393
+ }),
394
+ " ",
395
+ resolution.source === "saved" ? t("templates.editor.sourceSaved") : t("templates.editor.sourceBuiltIn"),
396
+ " ",
397
+ resolution.chain.length > 0 ? `(${[...resolution.chain, resolution.defaultLocale].join(" \u2192 ")})` : ""
398
+ ] }) : null
399
+ ]
400
+ }
401
+ )
402
+ ] }),
312
403
  revisions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.revisions, children: [
313
404
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: t("templates.editor.history") }),
314
405
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.list, children: revisions.map((rev) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: styles.row, children: [
@@ -365,7 +456,9 @@ var styles = {
365
456
  borderBottomColor: "var(--fonderie-text,#171717)",
366
457
  cursor: "default"
367
458
  },
368
- tabInactive: { ...tab, opacity: 0.55, textDecoration: "line-through" },
459
+ tabInactive: { opacity: 0.55, textDecoration: "line-through" },
460
+ // Fonderie's built-in copy, not saved by the app — the list's dashed chip.
461
+ tabBuiltIn: { fontStyle: "italic" },
369
462
  // Two columns, always: the editor on the left, the rendered result on the
370
463
  // right. Stacking the preview under a long form hid it below the fold.
371
464
  split: {
@@ -543,8 +636,14 @@ function TemplateListScreen({
543
636
  locale
544
637
  }) {
545
638
  const t = (0, import_client2.createAdminT)(locale);
546
- const { templates, isLoading, error } = (0, import_react_courier_admin2.useTemplates)(client);
547
- const groups = (0, import_client2.groupTemplatesByType)(templates);
639
+ const { catalog, isLoading, error } = (0, import_react_courier_admin2.useTemplateCatalog)(client);
640
+ const emails = catalog?.emails ?? [];
641
+ const defaultLocale = catalog?.defaultLocale ?? "en-US";
642
+ const chipStyle = (l) => ({
643
+ ...chip,
644
+ ...l.saved ? {} : chipBuiltIn,
645
+ ...l.active ? {} : chipOff
646
+ });
548
647
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
549
648
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
550
649
  "div",
@@ -558,8 +657,12 @@ function TemplateListScreen({
558
657
  type: "button",
559
658
  onClick: () => onCreateTemplate({
560
659
  locales: [
561
- ...new Set(templates.map((row) => row.locale).filter((l) => !!l))
562
- ]
660
+ ...new Set(
661
+ emails.flatMap(
662
+ (e) => (0, import_client2.templateLanguages)(e, defaultLocale).filter((l) => l.locale !== null).map((l) => l.label)
663
+ )
664
+ )
665
+ ].sort()
563
666
  }),
564
667
  style: styles2.newButton,
565
668
  children: t("templates.list.newTemplate")
@@ -569,39 +672,47 @@ function TemplateListScreen({
569
672
  }
570
673
  ),
571
674
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.hint, children: t("templates.list.hint") }),
572
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles2.list, children: groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: styles2.row, children: [
573
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
574
- "button",
575
- {
576
- type: "button",
577
- onClick: () => onSelectTemplate?.(group.primary),
578
- style: styles2.rowButton,
579
- children: [
580
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: group.type }),
581
- group.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : null,
582
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: group.primary.active ? styles2.active : styles2.inactive, children: [
675
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles2.list, children: emails.map((email) => {
677
+ const languages = (0, import_client2.templateLanguages)(email, defaultLocale);
678
+ const primary = languages.find((l) => l.locale === null) ?? languages[0];
679
+ const open = (l) => onSelectTemplate?.({
680
+ type: email.type,
681
+ locale: l?.locale ?? null,
682
+ system: email.system
683
+ });
684
+ return (
685
+ // Columns, not a flex row: the language chips line up down the
686
+ // list whatever the email name or badge.
687
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: styles2.row, children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { type: "button", onClick: () => open(primary), style: styles2.rowButton, children: [
689
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: email.type }),
690
+ email.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", {})
691
+ ] }),
692
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.chips, children: languages.map((l) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
693
+ "button",
694
+ {
695
+ type: "button",
696
+ onClick: () => open(l),
697
+ style: chipStyle(l),
698
+ "aria-label": t("templates.list.openLocale", {
699
+ type: email.type,
700
+ locale: l.label
701
+ }),
702
+ title: l.active ? void 0 : t("common.status.inactive"),
703
+ children: l.label
704
+ },
705
+ l.label
706
+ )) }),
707
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: primary?.active === false ? styles2.inactive : styles2.active, children: [
583
708
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
584
- group.primary.active ? t("common.status.active") : t("common.status.inactive")
709
+ primary?.active === false ? t("common.status.inactive") : t("common.status.active")
585
710
  ] })
586
- ]
587
- }
588
- ),
589
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.chips, children: group.entries.map((entry) => {
590
- const name = entry.locale ?? t("templates.defaultChip");
591
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
592
- "button",
593
- {
594
- type: "button",
595
- onClick: () => onSelectTemplate?.(entry),
596
- style: entry.active ? styles2.chip : styles2.chipInactive,
597
- "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
598
- title: entry.active ? void 0 : t("common.status.inactive"),
599
- children: name
600
- },
601
- entry.locale ?? ""
711
+ ] }, email.type)
602
712
  );
603
- }) })
604
- ] }, group.type)) })
713
+ }) }),
714
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.legend, children: t("templates.list.legend") })
715
+ ] })
605
716
  ] });
606
717
  }
607
718
  var pill = {
@@ -615,6 +726,7 @@ var pill = {
615
726
  lineHeight: 1.5,
616
727
  minWidth: 64
617
728
  };
729
+ var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
618
730
  var chip = {
619
731
  height: 24,
620
732
  boxSizing: "border-box",
@@ -624,9 +736,15 @@ var chip = {
624
736
  background: "var(--fonderie-surface,#fff)",
625
737
  color: "var(--fonderie-text,#171717)",
626
738
  fontSize: 12,
627
- fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
739
+ fontFamily: MONO,
628
740
  cursor: "pointer"
629
741
  };
742
+ var chipBuiltIn = {
743
+ borderStyle: "dashed",
744
+ background: "transparent",
745
+ color: "var(--fonderie-text-muted,#5c5c5c)"
746
+ };
747
+ var chipOff = { opacity: 0.5, textDecoration: "line-through" };
630
748
  var styles2 = {
631
749
  container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
632
750
  title: {
@@ -637,6 +755,7 @@ var styles2 = {
637
755
  lineHeight: 1.25
638
756
  },
639
757
  hint: { fontSize: 13.5, color: "var(--fonderie-text-muted,#5c5c5c)", margin: "4px 0 24px" },
758
+ legend: { fontSize: 12.5, color: "var(--fonderie-text-muted,#5c5c5c)", margin: "10px 2px 0" },
640
759
  status: { padding: "24px 0", color: "var(--fonderie-text-muted,#5c5c5c)", fontSize: 13.5 },
641
760
  error: { color: "var(--fonderie-danger,#e00)", marginBottom: 12, fontSize: 14 },
642
761
  list: {
@@ -649,20 +768,20 @@ var styles2 = {
649
768
  overflow: "hidden",
650
769
  boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
651
770
  },
771
+ // name + badge | languages | status — fixed tracks so every column aligns.
652
772
  row: {
653
- display: "flex",
773
+ display: "grid",
774
+ gridTemplateColumns: "minmax(0, 1fr) minmax(180px, 280px) 88px",
654
775
  alignItems: "center",
655
- gap: 8,
776
+ gap: 16,
656
777
  paddingRight: 16,
657
778
  borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)"
658
779
  },
659
780
  rowButton: {
660
- flex: 1,
661
781
  minWidth: 0,
662
782
  display: "flex",
663
- justifyContent: "space-between",
664
783
  alignItems: "center",
665
- gap: 16,
784
+ gap: 12,
666
785
  padding: "13px 16px",
667
786
  background: "none",
668
787
  border: "none",
@@ -674,12 +793,12 @@ var styles2 = {
674
793
  type: {
675
794
  fontSize: 13,
676
795
  fontWeight: 600,
677
- flex: 1,
678
- fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
796
+ overflow: "hidden",
797
+ textOverflow: "ellipsis",
798
+ whiteSpace: "nowrap",
799
+ fontFamily: MONO
679
800
  },
680
- chips: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: 6 },
681
- chip,
682
- chipInactive: { ...chip, opacity: 0.5, textDecoration: "line-through" },
801
+ chips: { display: "flex", flexWrap: "wrap", gap: 6 },
683
802
  dot: { width: 6, height: 6, borderRadius: 999, background: "currentColor" },
684
803
  builtIn: {
685
804
  fontSize: 11,
@@ -687,7 +806,8 @@ var styles2 = {
687
806
  padding: "1px 7px",
688
807
  borderRadius: 999,
689
808
  border: "1px solid var(--fonderie-border,#e0e0e0)",
690
- color: "var(--fonderie-text-muted,#5c5c5c)"
809
+ color: "var(--fonderie-text-muted,#5c5c5c)",
810
+ flexShrink: 0
691
811
  },
692
812
  newButton: {
693
813
  display: "inline-flex",
@@ -729,6 +849,7 @@ function TemplateCreateScreen({
729
849
  client,
730
850
  type,
731
851
  locales = [],
852
+ defaultLocale,
732
853
  onCreated,
733
854
  locale
734
855
  }) {
@@ -748,7 +869,7 @@ function TemplateCreateScreen({
748
869
  const variables = (0, import_react2.useRef)([]);
749
870
  (0, import_react2.useEffect)(() => {
750
871
  if (!type) return;
751
- client.getTemplate(type, null).then(({ result }) => {
872
+ client.getTemplate(type, null).catch(() => client.getBuiltInTemplate(type, null)).then(({ result }) => {
752
873
  setSubject(result.subject ?? "");
753
874
  setHtml(result.html ?? "");
754
875
  setText(result.text);
@@ -791,6 +912,9 @@ function TemplateCreateScreen({
791
912
  if (!TYPE_RE.test(t)) return setError(tr("templates.create.errorType"));
792
913
  if (addingLocale && !l) return setError(tr("templates.create.errorLocaleRequired"));
793
914
  if (l && !LOCALE_RE.test(l)) return setError(tr("templates.create.errorLocale"));
915
+ if (l && defaultLocale && l.toLowerCase() === defaultLocale.toLowerCase()) {
916
+ return setError(tr("templates.create.errorDefaultLocale", { locale: defaultLocale }));
917
+ }
794
918
  if (!text.trim()) return setError(tr("templates.create.errorText"));
795
919
  setBusy(true);
796
920
  try {
@@ -908,7 +1032,7 @@ function TemplateCreateScreen({
908
1032
  ] })
909
1033
  ] });
910
1034
  }
911
- var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
1035
+ var MONO2 = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
912
1036
  var styles3 = {
913
1037
  container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
914
1038
  // The editor's layout: the form on the left, the render pinned on the right.
@@ -941,7 +1065,7 @@ var styles3 = {
941
1065
  borderRadius: 8,
942
1066
  padding: 12,
943
1067
  fontSize: 13,
944
- fontFamily: MONO
1068
+ fontFamily: MONO2
945
1069
  },
946
1070
  // The console's warning token (the shell's amber), readable in both themes.
947
1071
  notice: {
@@ -994,7 +1118,7 @@ var styles3 = {
994
1118
  lineHeight: 1.55,
995
1119
  background: "var(--fonderie-surface,#fff)",
996
1120
  color: "var(--fonderie-text,#171717)",
997
- fontFamily: MONO
1121
+ fontFamily: MONO2
998
1122
  },
999
1123
  error: {
1000
1124
  color: "var(--fonderie-danger,#e00)",