@fonderie/vue-courier-admin-screens 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,24 +20,36 @@ 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
33
34
  var styles = {
34
- container: { padding: "24px" },
35
+ container: { padding: "8px 40px 64px", maxWidth: "1400px", boxSizing: "border-box" },
35
36
  // Two columns, always: editor left, rendered result right. Stacking the
36
37
  // preview under a long form hid it below the fold.
37
- split: { display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)", gap: "24px", alignItems: "start" },
38
+ split: {
39
+ display: "grid",
40
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
41
+ gap: "24px",
42
+ alignItems: "start"
43
+ },
38
44
  column: { minWidth: "0", display: "flex", flexDirection: "column" },
39
45
  // Pinned while the form scrolls, so the preview stays in view as you edit.
40
- previewColumn: { minWidth: "0", display: "flex", flexDirection: "column", position: "sticky", top: "16px" },
46
+ previewColumn: {
47
+ minWidth: "0",
48
+ display: "flex",
49
+ flexDirection: "column",
50
+ position: "sticky",
51
+ top: "16px"
52
+ },
41
53
  saveRow: { display: "flex", alignItems: "center", gap: "12px", marginTop: "16px" },
42
54
  previewHeader: {
43
55
  display: "flex",
@@ -51,7 +63,8 @@ var styles = {
51
63
  height: "520px",
52
64
  border: "1px solid var(--fonderie-border,#e0e0e0)",
53
65
  borderRadius: "8px",
54
- background: "var(--fonderie-surface,#fff)"
66
+ background: "var(--fonderie-surface,#fff)",
67
+ boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
55
68
  },
56
69
  previewText: {
57
70
  whiteSpace: "pre-wrap",
@@ -61,36 +74,117 @@ var styles = {
61
74
  fontSize: "13px",
62
75
  fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
63
76
  },
64
- title: { fontSize: "24px", fontWeight: 700 },
65
- subtitle: { fontSize: "16px", fontWeight: 600, marginBottom: "8px" },
66
- meta: { fontSize: "13px", color: "var(--fonderie-text-muted,#5c5c5c)", marginBottom: "16px" },
67
- status: { padding: "24px", textAlign: "center", color: "var(--fonderie-text-muted,#5c5c5c)" },
68
- error: { color: "var(--fonderie-danger,#e00)", marginBottom: "12px", fontSize: "14px" },
69
- list: { listStyle: "none", padding: "0", margin: "0" },
77
+ title: {
78
+ fontSize: "22px",
79
+ fontWeight: 600,
80
+ margin: "8px 0 4px",
81
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)",
82
+ lineHeight: 1.25
83
+ },
84
+ subtitle: {
85
+ fontSize: "15px",
86
+ fontWeight: 600,
87
+ margin: "0 0 10px",
88
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
89
+ },
90
+ meta: { fontSize: "13px", color: "var(--fonderie-text-muted,#5c5c5c)", marginBottom: "20px" },
91
+ status: { padding: "24px 0", color: "var(--fonderie-text-muted,#5c5c5c)", fontSize: "13.5px" },
92
+ error: {
93
+ color: "var(--fonderie-danger,#e00)",
94
+ background: "color-mix(in srgb, var(--fonderie-danger,#e00) 8%, transparent)",
95
+ borderRadius: "6px",
96
+ padding: "8px 12px",
97
+ margin: "8px 0",
98
+ fontSize: "13.5px"
99
+ },
100
+ list: {
101
+ listStyle: "none",
102
+ padding: "0",
103
+ margin: "0",
104
+ background: "var(--fonderie-surface,#fff)",
105
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
106
+ borderRadius: "var(--fonderie-radius-lg,8px)",
107
+ overflow: "hidden",
108
+ boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
109
+ },
70
110
  row: { borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)" },
71
111
  rowButton: {
72
112
  width: "100%",
73
113
  display: "flex",
74
114
  justifyContent: "space-between",
75
115
  alignItems: "center",
76
- padding: "12px 0",
116
+ gap: "16px",
117
+ padding: "13px 16px",
77
118
  background: "none",
78
119
  border: "none",
79
120
  cursor: "pointer",
80
- textAlign: "left"
121
+ textAlign: "left",
122
+ color: "var(--fonderie-text,#171717)",
123
+ fontFamily: "inherit"
124
+ },
125
+ type: {
126
+ fontSize: "13px",
127
+ fontWeight: 600,
128
+ flex: "1",
129
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
130
+ },
131
+ locale: { fontSize: "12.5px", color: "var(--fonderie-text-muted,#5c5c5c)" },
132
+ active: {
133
+ display: "inline-flex",
134
+ alignItems: "center",
135
+ gap: "6px",
136
+ borderRadius: "999px",
137
+ padding: "2px 9px 2px 8px",
138
+ fontSize: "12px",
139
+ fontWeight: 600,
140
+ lineHeight: 1.5,
141
+ minWidth: "64px",
142
+ color: "var(--fonderie-accent-strong,#009767)",
143
+ background: "color-mix(in srgb, var(--fonderie-accent-strong,#009767) 13%, transparent)"
144
+ },
145
+ inactive: {
146
+ display: "inline-flex",
147
+ alignItems: "center",
148
+ gap: "6px",
149
+ borderRadius: "999px",
150
+ padding: "2px 9px 2px 8px",
151
+ fontSize: "12px",
152
+ fontWeight: 600,
153
+ lineHeight: 1.5,
154
+ minWidth: "64px",
155
+ color: "var(--fonderie-text-muted,#5c5c5c)",
156
+ background: "color-mix(in srgb, var(--fonderie-text-muted,#5c5c5c) 10%, transparent)"
157
+ },
158
+ form: {
159
+ display: "flex",
160
+ flexDirection: "column",
161
+ gap: "4px",
162
+ background: "var(--fonderie-surface,#fff)",
163
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
164
+ borderRadius: "var(--fonderie-radius-lg,8px)",
165
+ padding: "6px 20px 20px",
166
+ boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
167
+ },
168
+ label: { fontSize: "13px", fontWeight: 500, marginTop: "14px", marginBottom: "2px" },
169
+ input: {
170
+ height: "36px",
171
+ boxSizing: "border-box",
172
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
173
+ borderRadius: "6px",
174
+ padding: "0 10px",
175
+ fontSize: "13.5px",
176
+ fontFamily: "inherit",
177
+ background: "var(--fonderie-surface,#fff)",
178
+ color: "var(--fonderie-text,#171717)"
81
179
  },
82
- type: { fontSize: "14px", fontWeight: 600, flex: "1" },
83
- locale: { fontSize: "13px", color: "var(--fonderie-text-muted,#5c5c5c)", marginRight: "16px" },
84
- active: { fontSize: "12px", color: "var(--fonderie-accent-strong,#009767)" },
85
- inactive: { fontSize: "12px", color: "var(--fonderie-text-muted,#5c5c5c)" },
86
- form: { display: "flex", flexDirection: "column", gap: "4px" },
87
- label: { fontSize: "13px", fontWeight: 600, marginTop: "12px" },
88
- input: { border: "1px solid var(--fonderie-border,#e0e0e0)", borderRadius: "8px", padding: "10px", fontSize: "14px" },
89
180
  textarea: {
90
181
  border: "1px solid var(--fonderie-border,#e0e0e0)",
91
- borderRadius: "8px",
182
+ borderRadius: "6px",
92
183
  padding: "10px",
93
- fontSize: "14px",
184
+ fontSize: "13px",
185
+ lineHeight: 1.55,
186
+ background: "var(--fonderie-surface,#fff)",
187
+ color: "var(--fonderie-text,#171717)",
94
188
  fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
95
189
  },
96
190
  checkboxLabel: {
@@ -101,58 +195,241 @@ var styles = {
101
195
  fontSize: "14px"
102
196
  },
103
197
  button: {
198
+ display: "inline-flex",
199
+ alignItems: "center",
200
+ height: "36px",
201
+ boxSizing: "border-box",
104
202
  backgroundColor: "var(--fonderie-text,#171717)",
105
203
  color: "var(--fonderie-surface,#fff)",
106
- padding: "10px 20px",
107
- borderRadius: "8px",
108
- border: "none",
109
- fontSize: "14px",
204
+ padding: "0 16px",
205
+ borderRadius: "6px",
206
+ border: "1px solid var(--fonderie-text,#171717)",
207
+ fontSize: "13.5px",
110
208
  fontWeight: 600,
111
- cursor: "pointer",
112
- alignSelf: "flex-start"
209
+ fontFamily: "inherit",
210
+ alignSelf: "flex-start",
211
+ cursor: "pointer"
113
212
  },
114
213
  buttonDisabled: {
115
- backgroundColor: "var(--fonderie-border,#e0e0e0)",
116
- color: "var(--fonderie-text-muted,#5c5c5c)",
117
- padding: "10px 20px",
118
- borderRadius: "8px",
119
- border: "none",
120
- fontSize: "14px",
214
+ display: "inline-flex",
215
+ alignItems: "center",
216
+ height: "36px",
217
+ boxSizing: "border-box",
218
+ backgroundColor: "var(--fonderie-text,#171717)",
219
+ color: "var(--fonderie-surface,#fff)",
220
+ padding: "0 16px",
221
+ borderRadius: "6px",
222
+ border: "1px solid var(--fonderie-text,#171717)",
223
+ fontSize: "13.5px",
121
224
  fontWeight: 600,
122
- cursor: "not-allowed",
123
- alignSelf: "flex-start"
225
+ fontFamily: "inherit",
226
+ alignSelf: "flex-start",
227
+ opacity: 0.45,
228
+ cursor: "not-allowed"
124
229
  },
125
230
  revisions: { marginTop: "32px" },
126
231
  revisionRow: {
127
232
  display: "flex",
128
233
  justifyContent: "space-between",
129
234
  alignItems: "center",
130
- padding: "8px 0",
235
+ gap: "12px",
236
+ padding: "9px 16px",
131
237
  borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)",
132
238
  fontSize: "13px"
133
239
  },
134
240
  rollbackButton: {
135
- background: "none",
241
+ display: "inline-flex",
242
+ alignItems: "center",
243
+ height: "28px",
244
+ boxSizing: "border-box",
245
+ background: "var(--fonderie-surface,#fff)",
246
+ color: "var(--fonderie-text,#171717)",
136
247
  border: "1px solid var(--fonderie-border,#e0e0e0)",
137
- borderRadius: "8px",
138
- padding: "4px 10px",
139
- fontSize: "12px",
248
+ borderRadius: "6px",
249
+ padding: "0 10px",
250
+ fontSize: "12.5px",
251
+ fontWeight: 500,
252
+ fontFamily: "inherit",
140
253
  cursor: "pointer"
141
- }
254
+ },
255
+ // Template list: matches @fonderie/vue-admin-screens' shell look with local
256
+ // values — this package sits below the admin screens and cannot import them.
257
+ listContainer: { padding: "32px 40px 64px", maxWidth: "1160px", boxSizing: "border-box" },
258
+ listTitle: {
259
+ fontSize: "22px",
260
+ fontWeight: 600,
261
+ margin: "0",
262
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)",
263
+ lineHeight: 1.25
264
+ },
265
+ hint: { fontSize: "13.5px", color: "var(--fonderie-text-muted,#5c5c5c)", margin: "4px 0 24px" },
266
+ dot: { width: "6px", height: "6px", borderRadius: "999px", background: "currentColor" }
142
267
  };
143
268
 
269
+ // src/screens/TemplateCreateScreen.ts
270
+ var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
271
+ var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
272
+ var TemplateCreateScreen = (0, import_vue.defineComponent)({
273
+ name: "FonderieTemplateCreateScreen",
274
+ props: {
275
+ client: { type: Object, required: true },
276
+ /**
277
+ * Given ⇒ add a LOCALE of this existing template: the type is fixed and the
278
+ * form starts from its default-locale copy. Omitted ⇒ a brand-new template
279
+ * type (one the app sends that has no stored copy yet).
280
+ */
281
+ type: { type: String, default: void 0 },
282
+ /** Locales already in use, offered as suggestions. */
283
+ locales: { type: Array, default: () => [] }
284
+ },
285
+ emits: {
286
+ /** What was created, so the caller can open it in the editor. */
287
+ created: (_created) => true
288
+ },
289
+ setup(props, { emit }) {
290
+ const addingLocale = props.type !== void 0;
291
+ const newType = (0, import_vue.ref)("");
292
+ const locale = (0, import_vue.ref)("");
293
+ const subject = (0, import_vue.ref)("");
294
+ const html = (0, import_vue.ref)("");
295
+ const text = (0, import_vue.ref)("");
296
+ const error = (0, import_vue.ref)(null);
297
+ const busy = (0, import_vue.ref)(false);
298
+ (0, import_vue.onMounted)(() => {
299
+ if (!props.type) return;
300
+ props.client.getTemplate(props.type, null).then(({ result }) => {
301
+ subject.value = result.subject ?? "";
302
+ html.value = result.html ?? "";
303
+ text.value = result.text;
304
+ }).catch(() => void 0);
305
+ });
306
+ async function submit(e) {
307
+ e.preventDefault();
308
+ error.value = null;
309
+ const t = addingLocale ? props.type : newType.value.trim();
310
+ const l = locale.value.trim() || null;
311
+ if (!TYPE_RE.test(t)) {
312
+ error.value = "Type: lowercase letters, digits, - and _ (e.g. weekly-digest).";
313
+ return;
314
+ }
315
+ if (addingLocale && !l) {
316
+ error.value = "Choose the locale to add, e.g. fr or fr-CA.";
317
+ return;
318
+ }
319
+ if (l && !LOCALE_RE.test(l)) {
320
+ error.value = "Locale: a language tag such as fr, es or fr-CA.";
321
+ return;
322
+ }
323
+ if (!text.value.trim()) {
324
+ error.value = "The plain-text body is required \u2014 it is what every email client can show.";
325
+ return;
326
+ }
327
+ busy.value = true;
328
+ try {
329
+ const exists = await props.client.getTemplate(t, l).then(
330
+ () => true,
331
+ (err) => !(err instanceof import_client.FonderieApiError && err.status === 404)
332
+ );
333
+ if (exists) {
334
+ error.value = `"${t}"${l ? ` (${l})` : ""} already exists \u2014 open it from the list to edit.`;
335
+ return;
336
+ }
337
+ const input = { text: text.value, active: true };
338
+ if (subject.value) input.subject = subject.value;
339
+ if (html.value) input.html = html.value;
340
+ await props.client.setTemplate(t, input, l);
341
+ emit("created", { type: t, locale: l });
342
+ } catch (err) {
343
+ error.value = err instanceof import_client.FonderieApiError ? err.explanation : String(err);
344
+ } finally {
345
+ busy.value = false;
346
+ }
347
+ }
348
+ const field = (id, label, model, opts = {}) => [
349
+ (0, import_vue.h)("label", { style: styles.label, for: id }, label),
350
+ (0, import_vue.h)(opts.textarea ? "textarea" : "input", {
351
+ id,
352
+ style: opts.textarea ? { ...styles.textarea, minHeight: opts.minHeight } : styles.input,
353
+ value: model.value,
354
+ autocomplete: "off",
355
+ spellcheck: false,
356
+ ...opts.placeholder ? { placeholder: opts.placeholder } : {},
357
+ ...opts.list ? { list: opts.list } : {},
358
+ onInput: (ev) => {
359
+ model.value = ev.target.value;
360
+ }
361
+ })
362
+ ];
363
+ return () => (0, import_vue.h)("div", { style: { ...styles.container, maxWidth: "760px" } }, [
364
+ (0, import_vue.h)(
365
+ "h1",
366
+ { style: styles.title },
367
+ addingLocale ? `Add a locale to ${props.type}` : "New template"
368
+ ),
369
+ (0, import_vue.h)(
370
+ "p",
371
+ { style: { ...styles.meta, lineHeight: 1.5 } },
372
+ addingLocale ? "Starts from the default copy. People whose locale matches receive this version; everyone else keeps the default." : "For an email your app sends that has no stored copy yet. The type must match what the app sends."
373
+ ),
374
+ (0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
375
+ ...addingLocale ? [] : field("new-template-type", "Type", newType, { placeholder: "weekly-digest" }),
376
+ ...field(
377
+ "new-template-locale",
378
+ addingLocale ? "Locale" : "Locale (optional \u2014 empty is the default)",
379
+ locale,
380
+ { placeholder: "fr", list: "new-template-locales" }
381
+ ),
382
+ (0, import_vue.h)(
383
+ "datalist",
384
+ { id: "new-template-locales" },
385
+ [...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
386
+ ),
387
+ ...field("new-template-subject", "Subject", subject),
388
+ ...field("new-template-html", "HTML body", html, { textarea: true, minHeight: "160px" }),
389
+ ...field("new-template-text", "Plain-text body", text, {
390
+ textarea: true,
391
+ minHeight: "100px"
392
+ }),
393
+ error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
394
+ (0, import_vue.h)(
395
+ "button",
396
+ {
397
+ type: "submit",
398
+ disabled: busy.value,
399
+ style: { ...styles.button, marginTop: "16px" }
400
+ },
401
+ busy.value ? "Creating\u2026" : addingLocale ? "Add locale" : "Create template"
402
+ )
403
+ ])
404
+ ]);
405
+ }
406
+ });
407
+
144
408
  // src/screens/TemplateEditorScreen.ts
409
+ var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
410
+ var import_vue2 = require("vue");
145
411
  var PREVIEW_DEBOUNCE_MS = 500;
146
412
  var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
147
- var TemplateEditorScreen = (0, import_vue.defineComponent)({
413
+ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
148
414
  name: "FonderieTemplateEditorScreen",
149
415
  props: {
150
416
  client: { type: Object, required: true },
151
417
  type: { type: String, required: true },
152
- locale: { type: String, default: null }
418
+ locale: { type: String, default: null },
419
+ /**
420
+ * A built-in email's default-locale row (the list's `system` flag): it can
421
+ * be edited and rolled back, never deleted — Delete is not offered.
422
+ */
423
+ system: { type: Boolean, default: false },
424
+ /** Shows "+ Add locale" on the default locale (emits add-locale). */
425
+ allowAddLocale: { type: Boolean, default: false }
153
426
  },
154
427
  emits: {
155
- saved: () => true
428
+ saved: () => true,
429
+ /** The template was deleted and no longer exists. */
430
+ deleted: () => true,
431
+ /** Create a translation of this template. */
432
+ "add-locale": (_type) => true
156
433
  },
157
434
  setup(props, { emit }) {
158
435
  const { template, isLoading, error, refresh } = (0, import_vue_courier_admin.useTemplate)(
@@ -160,15 +437,16 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
160
437
  props.type,
161
438
  props.locale
162
439
  );
163
- const { saveTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
440
+ const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin.useTemplates)(props.client);
441
+ const isDeleting = (0, import_vue2.ref)(false);
164
442
  const { revisions, rollback } = (0, import_vue_courier_admin.useTemplateRevisions)(props.client, props.type, props.locale);
165
- const isSaving = (0, import_vue.ref)(false);
166
- const saveError = (0, import_vue.ref)(null);
167
- const subject = (0, import_vue.ref)("");
168
- const html = (0, import_vue.ref)("");
169
- const text = (0, import_vue.ref)("");
170
- const active = (0, import_vue.ref)(true);
171
- (0, import_vue.watch)(template, (tpl) => {
443
+ const isSaving = (0, import_vue2.ref)(false);
444
+ const saveError = (0, import_vue2.ref)(null);
445
+ const subject = (0, import_vue2.ref)("");
446
+ const html = (0, import_vue2.ref)("");
447
+ const text = (0, import_vue2.ref)("");
448
+ const active = (0, import_vue2.ref)(true);
449
+ (0, import_vue2.watch)(template, (tpl) => {
172
450
  if (!tpl) return;
173
451
  subject.value = tpl.subject ?? "";
174
452
  html.value = tpl.html ?? "";
@@ -181,8 +459,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
181
459
  error: previewError,
182
460
  renderPreview
183
461
  } = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
184
- const sampleJson = (0, import_vue.ref)("{}");
185
- const sampleError = (0, import_vue.ref)(null);
462
+ const sampleJson = (0, import_vue2.ref)("{}");
463
+ const sampleError = (0, import_vue2.ref)(null);
186
464
  async function run() {
187
465
  let data = {};
188
466
  try {
@@ -217,12 +495,12 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
217
495
  }
218
496
  }
219
497
  let previewTimer;
220
- (0, import_vue.watch)([template, subject, html, text, sampleJson], () => {
498
+ (0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
221
499
  if (!template.value) return;
222
500
  clearTimeout(previewTimer);
223
501
  previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS);
224
502
  });
225
- const dirty = (0, import_vue.computed)(() => {
503
+ const dirty = (0, import_vue2.computed)(() => {
226
504
  const tpl = template.value;
227
505
  return !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
228
506
  });
@@ -244,13 +522,28 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
244
522
  isSaving.value = false;
245
523
  }
246
524
  }
525
+ async function handleDelete() {
526
+ const which = props.locale ? `the ${props.locale} version of "${props.type}"` : `"${props.type}"`;
527
+ const fallback = props.locale ? " People in that locale will receive the default version." : "";
528
+ if (!window.confirm(`Delete ${which}?${fallback}`)) return;
529
+ isDeleting.value = true;
530
+ saveError.value = null;
531
+ try {
532
+ await removeTemplate(props.type, props.locale);
533
+ emit("deleted");
534
+ } catch (err) {
535
+ saveError.value = err;
536
+ } finally {
537
+ isDeleting.value = false;
538
+ }
539
+ }
247
540
  function renderRevision(rev) {
248
- return (0, import_vue.h)("li", { key: rev.version, style: styles.revisionRow }, [
249
- (0, import_vue.h)(
541
+ return (0, import_vue2.h)("li", { key: rev.version, style: styles.revisionRow }, [
542
+ (0, import_vue2.h)(
250
543
  "span",
251
544
  `v${rev.version} \u2014 ${rev.actor ?? "unknown"} \u2014 ${new Date(rev.createdAt).toLocaleString()}`
252
545
  ),
253
- (0, import_vue.h)(
546
+ (0, import_vue2.h)(
254
547
  "button",
255
548
  { type: "button", style: styles.rollbackButton, onClick: () => rollback(rev.version) },
256
549
  "Roll back"
@@ -258,21 +551,36 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
258
551
  ]);
259
552
  }
260
553
  return () => {
261
- if (isLoading.value) return (0, import_vue.h)("p", { style: styles.status }, "Loading template\u2026");
554
+ if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, "Loading template\u2026");
262
555
  if (error.value)
263
- return (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
264
- return (0, import_vue.h)("div", { style: styles.container }, [
265
- (0, import_vue.h)("h1", { style: styles.title }, props.type),
266
- (0, import_vue.h)(
267
- "p",
268
- { style: styles.meta },
269
- `${props.locale ?? "base"} \xB7 v${template.value?.version ?? 1}`
556
+ return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
557
+ return (0, import_vue2.h)("div", { style: styles.container }, [
558
+ (0, import_vue2.h)(
559
+ "div",
560
+ { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
561
+ [
562
+ (0, import_vue2.h)("h1", { style: styles.title }, props.type),
563
+ props.allowAddLocale && !props.locale ? (0, import_vue2.h)(
564
+ "button",
565
+ {
566
+ type: "button",
567
+ style: { ...styles.rollbackButton, marginLeft: "auto" },
568
+ onClick: () => emit("add-locale", props.type)
569
+ },
570
+ "+ Add locale"
571
+ ) : null
572
+ ]
270
573
  ),
271
- (0, import_vue.h)("div", { style: styles.split }, [
272
- (0, import_vue.h)("div", { style: styles.column }, [
273
- (0, import_vue.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
274
- (0, import_vue.h)("label", { style: styles.label, for: "template-subject" }, "Subject"),
275
- (0, import_vue.h)("input", {
574
+ (0, import_vue2.h)("p", { style: styles.meta }, [
575
+ (0, import_vue2.h)("strong", props.locale ?? "default locale"),
576
+ ` \xB7 v${template.value?.version ?? 1}`,
577
+ props.system ? " \xB7 built-in email: edit or roll back, it cannot be deleted" : ""
578
+ ]),
579
+ (0, import_vue2.h)("div", { style: styles.split }, [
580
+ (0, import_vue2.h)("div", { style: styles.column }, [
581
+ (0, import_vue2.h)("form", { style: styles.form, onSubmit: handleSubmit }, [
582
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-subject" }, "Subject"),
583
+ (0, import_vue2.h)("input", {
276
584
  id: "template-subject",
277
585
  style: styles.input,
278
586
  value: subject.value,
@@ -280,8 +588,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
280
588
  subject.value = e.target.value;
281
589
  }
282
590
  }),
283
- (0, import_vue.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
284
- (0, import_vue.h)("textarea", {
591
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-html" }, "HTML body"),
592
+ (0, import_vue2.h)("textarea", {
285
593
  id: "template-html",
286
594
  style: styles.textarea,
287
595
  rows: 8,
@@ -290,8 +598,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
290
598
  html.value = e.target.value;
291
599
  }
292
600
  }),
293
- (0, import_vue.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
294
- (0, import_vue.h)("textarea", {
601
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-text" }, "Plain-text body"),
602
+ (0, import_vue2.h)("textarea", {
295
603
  id: "template-text",
296
604
  style: styles.textarea,
297
605
  rows: 4,
@@ -301,8 +609,8 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
301
609
  text.value = e.target.value;
302
610
  }
303
611
  }),
304
- (0, import_vue.h)("label", { style: styles.checkboxLabel }, [
305
- (0, import_vue.h)("input", {
612
+ (0, import_vue2.h)("label", { style: styles.checkboxLabel }, [
613
+ (0, import_vue2.h)("input", {
306
614
  type: "checkbox",
307
615
  checked: active.value,
308
616
  onChange: (e) => {
@@ -311,9 +619,23 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
311
619
  }),
312
620
  "Active"
313
621
  ]),
314
- saveError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
315
- (0, import_vue.h)("div", { style: styles.saveRow }, [
316
- (0, import_vue.h)(
622
+ saveError.value?.reason === "VERSION_CONFLICT" ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, [
623
+ "Someone changed this template since you opened it. Reload to see their change, then edit again. ",
624
+ (0, import_vue2.h)(
625
+ "button",
626
+ {
627
+ type: "button",
628
+ style: styles.rollbackButton,
629
+ onClick: () => {
630
+ saveError.value = null;
631
+ void refresh();
632
+ }
633
+ },
634
+ "Reload"
635
+ )
636
+ ]) : saveError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, saveError.value.explanation) : null,
637
+ (0, import_vue2.h)("div", { style: styles.saveRow }, [
638
+ (0, import_vue2.h)(
317
639
  "button",
318
640
  {
319
641
  type: "submit",
@@ -322,11 +644,27 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
322
644
  },
323
645
  isSaving.value ? "Saving\u2026" : "Save"
324
646
  ),
325
- !dirty.value && !isSaving.value ? (0, import_vue.h)("span", { style: styles.meta }, "No changes to save") : null
647
+ !dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, "No changes to save") : null,
648
+ !props.system ? (0, import_vue2.h)(
649
+ "button",
650
+ {
651
+ type: "button",
652
+ disabled: isDeleting.value,
653
+ onClick: () => void handleDelete(),
654
+ style: {
655
+ ...styles.rollbackButton,
656
+ height: "36px",
657
+ marginLeft: "auto",
658
+ color: "var(--fonderie-danger,#e00)",
659
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 40%, transparent)"
660
+ }
661
+ },
662
+ isDeleting.value ? "Deleting\u2026" : "Delete"
663
+ ) : null
326
664
  ])
327
665
  ]),
328
- (0, import_vue.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
329
- (0, import_vue.h)("textarea", {
666
+ (0, import_vue2.h)("label", { style: styles.label, for: "template-sample" }, "Sample data"),
667
+ (0, import_vue2.h)("textarea", {
330
668
  id: "template-sample",
331
669
  style: styles.textarea,
332
670
  rows: 6,
@@ -336,31 +674,31 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
336
674
  sampleJson.value = e.target.value;
337
675
  }
338
676
  }),
339
- sampleError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
677
+ sampleError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, sampleError.value) : null
340
678
  ]),
341
- (0, import_vue.h)("div", { style: styles.previewColumn }, [
342
- (0, import_vue.h)("div", { style: styles.previewHeader }, [
343
- (0, import_vue.h)("span", { style: styles.label }, "Preview"),
344
- (0, import_vue.h)("span", { style: styles.meta }, isPreviewing.value ? "Rendering\u2026" : "Live")
679
+ (0, import_vue2.h)("div", { style: styles.previewColumn }, [
680
+ (0, import_vue2.h)("div", { style: styles.previewHeader }, [
681
+ (0, import_vue2.h)("span", { style: styles.label }, "Preview"),
682
+ (0, import_vue2.h)("span", { style: styles.meta }, isPreviewing.value ? "Rendering\u2026" : "Live")
345
683
  ]),
346
- previewError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
347
- preview.value?.subject ? (0, import_vue.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
684
+ previewError.value ? (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
685
+ preview.value?.subject ? (0, import_vue2.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
348
686
  preview.value?.html ? (
349
687
  // sandbox= — no scripts, no same-origin. Operator-authored HTML
350
688
  // must never execute in the dashboard's origin, where the admin
351
689
  // token lives.
352
- (0, import_vue.h)("iframe", {
690
+ (0, import_vue2.h)("iframe", {
353
691
  title: "Template preview",
354
692
  style: styles.previewFrame,
355
693
  sandbox: "",
356
694
  srcdoc: preview.value.html
357
695
  })
358
- ) : preview.value ? (0, import_vue.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue.h)("p", { style: styles.meta }, "Nothing rendered yet.")
696
+ ) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, "Nothing rendered yet.")
359
697
  ])
360
698
  ]),
361
- revisions.value.length > 0 ? (0, import_vue.h)("div", { style: styles.revisions }, [
362
- (0, import_vue.h)("h2", { style: styles.subtitle }, "History"),
363
- (0, import_vue.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
699
+ revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
700
+ (0, import_vue2.h)("h2", { style: styles.subtitle }, "History"),
701
+ (0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
364
702
  ]) : null
365
703
  ]);
366
704
  };
@@ -369,20 +707,48 @@ var TemplateEditorScreen = (0, import_vue.defineComponent)({
369
707
 
370
708
  // src/screens/TemplateListScreen.ts
371
709
  var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
372
- var import_vue2 = require("vue");
373
- var TemplateListScreen = (0, import_vue2.defineComponent)({
710
+ var import_vue3 = require("vue");
711
+ var builtIn = {
712
+ fontSize: "11px",
713
+ fontWeight: 500,
714
+ padding: "1px 7px",
715
+ borderRadius: "999px",
716
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
717
+ color: "var(--fonderie-text-muted,#5c5c5c)"
718
+ };
719
+ var newButton = {
720
+ display: "inline-flex",
721
+ alignItems: "center",
722
+ height: "32px",
723
+ boxSizing: "border-box",
724
+ borderRadius: "6px",
725
+ padding: "0 12px",
726
+ fontSize: "13px",
727
+ fontWeight: 500,
728
+ fontFamily: "inherit",
729
+ cursor: "pointer",
730
+ backgroundColor: "var(--fonderie-text,#171717)",
731
+ color: "var(--fonderie-surface,#fff)",
732
+ border: "1px solid var(--fonderie-text,#171717)"
733
+ };
734
+ var TemplateListScreen = (0, import_vue3.defineComponent)({
374
735
  name: "FonderieTemplateListScreen",
375
736
  props: {
376
- client: { type: Object, required: true }
737
+ client: { type: Object, required: true },
738
+ /** Shows a "New template" button (emits create-template). */
739
+ allowCreate: { type: Boolean, default: false }
377
740
  },
378
741
  emits: {
379
- "select-template": (_template) => true
742
+ /** The whole row — type AND locale, and whether it is built-in. */
743
+ "select-template": (_template) => true,
744
+ /** Receives the locales in use, to suggest. */
745
+ "create-template": (_context) => true
380
746
  },
381
747
  setup(props, { emit }) {
382
748
  const { templates, isLoading, error } = (0, import_vue_courier_admin2.useTemplates)(props.client);
383
749
  function renderRow(template) {
384
- return (0, import_vue2.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
385
- (0, import_vue2.h)(
750
+ return (0, import_vue3.h)("li", { key: `${template.type}:${template.locale ?? "base"}`, style: styles.row }, [
751
+ (0, import_vue3.h)(
386
752
  "button",
387
753
  {
388
754
  type: "button",
@@ -390,30 +756,59 @@ var TemplateListScreen = (0, import_vue2.defineComponent)({
390
756
  onClick: () => emit("select-template", template)
391
757
  },
392
758
  [
393
- (0, import_vue2.h)("span", { style: styles.type }, template.type),
394
- (0, import_vue2.h)("span", { style: styles.locale }, template.locale ?? "base"),
395
- (0, import_vue2.h)(
396
- "span",
397
- { style: template.active ? styles.active : styles.inactive },
398
- template.active ? "Active" : "Inactive"
399
- )
759
+ (0, import_vue3.h)("span", { style: styles.type }, template.type),
760
+ (0, import_vue3.h)("span", { style: styles.locale }, template.locale ?? "default locale"),
761
+ template.system ? (0, import_vue3.h)("span", { style: builtIn }, "built-in") : null,
762
+ (0, import_vue3.h)("span", { style: template.active ? styles.active : styles.inactive }, [
763
+ (0, import_vue3.h)("span", { style: styles.dot }),
764
+ template.active ? "active" : "inactive"
765
+ ])
400
766
  ]
401
767
  )
402
768
  ]);
403
769
  }
404
- return () => {
405
- if (isLoading.value) return (0, import_vue2.h)("p", { style: styles.status }, "Loading templates\u2026");
406
- if (error.value)
407
- return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
408
- return (0, import_vue2.h)("div", { style: styles.container }, [
409
- (0, import_vue2.h)("h1", { style: styles.title }, "Templates"),
410
- (0, import_vue2.h)("ul", { style: styles.list }, templates.value.map(renderRow))
411
- ]);
412
- };
770
+ return () => (0, import_vue3.h)("div", { style: styles.listContainer }, [
771
+ (0, import_vue3.h)(
772
+ "div",
773
+ {
774
+ style: {
775
+ display: "flex",
776
+ alignItems: "center",
777
+ justifyContent: "space-between",
778
+ gap: "16px"
779
+ }
780
+ },
781
+ [
782
+ (0, import_vue3.h)("h1", { style: styles.listTitle }, "Templates"),
783
+ props.allowCreate ? (0, import_vue3.h)(
784
+ "button",
785
+ {
786
+ type: "button",
787
+ style: newButton,
788
+ onClick: () => emit("create-template", {
789
+ locales: [
790
+ ...new Set(
791
+ templates.value.map((t) => t.locale).filter((l) => !!l)
792
+ )
793
+ ]
794
+ })
795
+ },
796
+ "New template"
797
+ ) : null
798
+ ]
799
+ ),
800
+ (0, import_vue3.h)(
801
+ "p",
802
+ { style: styles.hint },
803
+ "Every email the app sends. Open one to edit its copy and preview it live."
804
+ ),
805
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, "Loading templates\u2026") : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue3.h)("ul", { style: styles.list }, templates.value.map(renderRow))
806
+ ]);
413
807
  }
414
808
  });
415
809
  // Annotate the CommonJS export names for ESM import in node:
416
810
  0 && (module.exports = {
811
+ TemplateCreateScreen,
417
812
  TemplateEditorScreen,
418
813
  TemplateListScreen
419
814
  });