@fonderie/react-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
@@ -41,11 +41,13 @@ function TemplateEditorScreen({
41
41
  system = false,
42
42
  onDeleted,
43
43
  onAddLocale,
44
+ onSelectLocale,
44
45
  uiLocale
45
46
  }) {
46
47
  const t = (0, import_react.useMemo)(() => (0, import_client.createAdminT)(uiLocale), [uiLocale]);
47
48
  const { template, isLoading, error, refresh } = (0, import_react_courier_admin.useTemplate)(client, type, locale);
48
- const { saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
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
51
  const [isDeleting, setIsDeleting] = (0, import_react.useState)(false);
50
52
  const { revisions, rollback } = (0, import_react_courier_admin.useTemplateRevisions)(client, type, locale);
51
53
  const [isSaving, setIsSaving] = (0, import_react.useState)(false);
@@ -116,6 +118,10 @@ function TemplateEditorScreen({
116
118
  setIsSaving(false);
117
119
  }
118
120
  };
121
+ const switchTo = (entry) => {
122
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
123
+ onSelectLocale?.(entry);
124
+ };
119
125
  const handleDelete = async () => {
120
126
  const message = locale ? t("templates.editor.confirmDeleteLocale", { type, locale }) : t("templates.editor.confirmDelete", { type });
121
127
  if (!window.confirm(message)) return;
@@ -134,18 +140,40 @@ function TemplateEditorScreen({
134
140
  if (error)
135
141
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: error.explanation });
136
142
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.container, children: [
137
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
138
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
139
- onAddLocale && !locale ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
144
+ onSelectLocale || onAddLocale ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.tabBar, children: [
145
+ onSelectLocale ? (
146
+ // Each tab opens another stored row, so this is navigation, not an
147
+ // 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);
150
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
151
+ "button",
152
+ {
153
+ type: "button",
154
+ "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")
159
+ },
160
+ entry.locale ?? ""
161
+ );
162
+ }) })
163
+ ) : null,
164
+ onAddLocale ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
140
165
  "button",
141
166
  {
142
167
  type: "button",
143
168
  style: { ...styles.rollbackButton, marginLeft: "auto" },
144
- onClick: () => onAddLocale(type),
169
+ onClick: () => {
170
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
171
+ onAddLocale(type, { locales: (0, import_client.missingTemplateLocales)(templates, type) });
172
+ },
145
173
  children: t("templates.editor.addLocale")
146
174
  }
147
175
  ) : null
148
- ] }),
176
+ ] }) : null,
149
177
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.meta, children: [
150
178
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? t("templates.defaultLocale") }),
151
179
  " \xB7 v",
@@ -306,8 +334,38 @@ function TemplateEditorScreen({
306
334
  ] })
307
335
  ] });
308
336
  }
337
+ var tab = {
338
+ height: 32,
339
+ padding: "0 10px",
340
+ background: "none",
341
+ border: "none",
342
+ borderBottom: "2px solid transparent",
343
+ color: "var(--fonderie-text-muted,#5c5c5c)",
344
+ fontSize: 13,
345
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
346
+ cursor: "pointer"
347
+ };
309
348
  var styles = {
310
349
  container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
350
+ // One tab per locale of this email, underlined like a document's tabs.
351
+ tabBar: {
352
+ display: "flex",
353
+ alignItems: "flex-end",
354
+ gap: 12,
355
+ borderBottom: "1px solid var(--fonderie-border,#e0e0e0)",
356
+ margin: "4px 0 12px",
357
+ paddingBottom: 6
358
+ },
359
+ tabs: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: -7 },
360
+ tab,
361
+ tabSelected: {
362
+ ...tab,
363
+ color: "var(--fonderie-text,#171717)",
364
+ fontWeight: 600,
365
+ borderBottomColor: "var(--fonderie-text,#171717)",
366
+ cursor: "default"
367
+ },
368
+ tabInactive: { ...tab, opacity: 0.55, textDecoration: "line-through" },
311
369
  // Two columns, always: the editor on the left, the rendered result on the
312
370
  // right. Stacking the preview under a long form hid it below the fold.
313
371
  split: {
@@ -486,6 +544,7 @@ function TemplateListScreen({
486
544
  }) {
487
545
  const t = (0, import_client2.createAdminT)(locale);
488
546
  const { templates, isLoading, error } = (0, import_react_courier_admin2.useTemplates)(client);
547
+ const groups = (0, import_client2.groupTemplatesByType)(templates);
489
548
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
490
549
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
491
550
  "div",
@@ -510,23 +569,39 @@ function TemplateListScreen({
510
569
  }
511
570
  ),
512
571
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.hint, children: t("templates.list.hint") }),
513
- 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: templates.map((template) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
514
- "button",
515
- {
516
- type: "button",
517
- onClick: () => onSelectTemplate?.(template),
518
- style: styles2.rowButton,
519
- children: [
520
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: template.type }),
521
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.locale, children: template.locale ?? t("templates.defaultLocale") }),
522
- template.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : null,
523
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: template.active ? styles2.active : styles2.inactive, children: [
524
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
525
- template.active ? t("common.status.active") : t("common.status.inactive")
526
- ] })
527
- ]
528
- }
529
- ) }, `${template.type}:${template.locale ?? "base"}`)) })
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: [
583
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
584
+ group.primary.active ? t("common.status.active") : t("common.status.inactive")
585
+ ] })
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 ?? ""
602
+ );
603
+ }) })
604
+ ] }, group.type)) })
530
605
  ] });
531
606
  }
532
607
  var pill = {
@@ -540,6 +615,18 @@ var pill = {
540
615
  lineHeight: 1.5,
541
616
  minWidth: 64
542
617
  };
618
+ var chip = {
619
+ height: 24,
620
+ boxSizing: "border-box",
621
+ padding: "0 8px",
622
+ borderRadius: 6,
623
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
624
+ background: "var(--fonderie-surface,#fff)",
625
+ color: "var(--fonderie-text,#171717)",
626
+ fontSize: 12,
627
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
628
+ cursor: "pointer"
629
+ };
543
630
  var styles2 = {
544
631
  container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
545
632
  title: {
@@ -562,9 +649,16 @@ var styles2 = {
562
649
  overflow: "hidden",
563
650
  boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
564
651
  },
565
- row: { borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)" },
652
+ row: {
653
+ display: "flex",
654
+ alignItems: "center",
655
+ gap: 8,
656
+ paddingRight: 16,
657
+ borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)"
658
+ },
566
659
  rowButton: {
567
- width: "100%",
660
+ flex: 1,
661
+ minWidth: 0,
568
662
  display: "flex",
569
663
  justifyContent: "space-between",
570
664
  alignItems: "center",
@@ -583,7 +677,9 @@ var styles2 = {
583
677
  flex: 1,
584
678
  fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
585
679
  },
586
- locale: { fontSize: 12.5, color: "var(--fonderie-text-muted,#5c5c5c)" },
680
+ chips: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: 6 },
681
+ chip,
682
+ chipInactive: { ...chip, opacity: 0.5, textDecoration: "line-through" },
587
683
  dot: { width: 6, height: 6, borderRadius: 999, background: "currentColor" },
588
684
  builtIn: {
589
685
  fontSize: 11,
@@ -622,10 +718,13 @@ var styles2 = {
622
718
 
623
719
  // src/screens/TemplateCreateScreen.tsx
624
720
  var import_client3 = require("@fonderie/client");
721
+ var import_react_courier_admin3 = require("@fonderie/react-courier-admin");
625
722
  var import_react2 = require("react");
626
723
  var import_jsx_runtime3 = require("react/jsx-runtime");
627
724
  var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
628
725
  var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
726
+ var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
727
+ var PREVIEW_DEBOUNCE_MS2 = 500;
629
728
  function TemplateCreateScreen({
630
729
  client,
631
730
  type,
@@ -642,14 +741,48 @@ function TemplateCreateScreen({
642
741
  const [text, setText] = (0, import_react2.useState)("");
643
742
  const [error, setError] = (0, import_react2.useState)(null);
644
743
  const [busy, setBusy] = (0, import_react2.useState)(false);
744
+ const [source, setSource] = (0, import_react2.useState)(
745
+ null
746
+ );
747
+ const { preview, isPreviewing, error: previewError, renderPreview } = (0, import_react_courier_admin3.useTemplatePreview)(client);
748
+ const variables = (0, import_react2.useRef)([]);
645
749
  (0, import_react2.useEffect)(() => {
646
750
  if (!type) return;
647
751
  client.getTemplate(type, null).then(({ result }) => {
648
752
  setSubject(result.subject ?? "");
649
753
  setHtml(result.html ?? "");
650
754
  setText(result.text);
755
+ setSource({ subject: result.subject ?? "", html: result.html ?? "", text: result.text });
651
756
  }).catch(() => void 0);
652
757
  }, [client, type]);
758
+ const previewType = addingLocale ? type : newType.trim();
759
+ const previewLocale = LOCALE_RE.test(targetLocale.trim()) ? targetLocale.trim() : null;
760
+ (0, import_react2.useEffect)(() => {
761
+ if (!TYPE_RE.test(previewType) || !text.trim()) return;
762
+ const timer = setTimeout(async () => {
763
+ const input = { text, ...subject ? { subject } : {}, ...html ? { html } : {} };
764
+ const render = () => renderPreview(
765
+ previewType,
766
+ { ...input, data: Object.fromEntries(variables.current.map((v) => [v, v])) },
767
+ previewLocale
768
+ );
769
+ try {
770
+ const result = await render();
771
+ const used = result.variables.filter((v) => !IMPLICIT2.has(v));
772
+ if (used.some((v) => !variables.current.includes(v))) {
773
+ variables.current = used;
774
+ await render();
775
+ }
776
+ } catch {
777
+ }
778
+ }, PREVIEW_DEBOUNCE_MS2);
779
+ return () => clearTimeout(timer);
780
+ }, [renderPreview, previewType, previewLocale, subject, html, text]);
781
+ const untranslated = source ? [
782
+ subject && subject === source.subject ? tr("templates.subject") : null,
783
+ html && html === source.html ? tr("templates.htmlBody") : null,
784
+ text === source.text ? tr("templates.textBody") : null
785
+ ].filter((f) => f !== null) : [];
653
786
  const submit = async (e) => {
654
787
  e.preventDefault();
655
788
  setError(null);
@@ -685,75 +818,140 @@ function TemplateCreateScreen({
685
818
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.container, children: [
686
819
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: styles3.title, children: addingLocale ? tr("templates.create.titleLocale", { type }) : tr("templates.create.titleNew") }),
687
820
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew") }),
688
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
689
- !addingLocale ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
690
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
821
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.split, children: [
822
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
823
+ !addingLocale ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
824
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
826
+ "input",
827
+ {
828
+ id: "new-template-type",
829
+ style: styles3.input,
830
+ value: newType,
831
+ placeholder: "weekly-digest",
832
+ autoComplete: "off",
833
+ spellCheck: false,
834
+ onChange: (e) => setNewType(e.target.value)
835
+ }
836
+ )
837
+ ] }) : null,
838
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
691
839
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
692
840
  "input",
693
841
  {
694
- id: "new-template-type",
842
+ id: "new-template-locale",
695
843
  style: styles3.input,
696
- value: newType,
697
- placeholder: "weekly-digest",
844
+ value: targetLocale,
845
+ placeholder: "fr",
846
+ list: "new-template-locales",
698
847
  autoComplete: "off",
699
848
  spellCheck: false,
700
- onChange: (e) => setNewType(e.target.value)
849
+ onChange: (e) => setTargetLocale(e.target.value)
701
850
  }
702
- )
703
- ] }) : null,
704
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
705
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
706
- "input",
707
- {
708
- id: "new-template-locale",
709
- style: styles3.input,
710
- value: targetLocale,
711
- placeholder: "fr",
712
- list: "new-template-locales",
713
- autoComplete: "off",
714
- spellCheck: false,
715
- onChange: (e) => setTargetLocale(e.target.value)
716
- }
717
- ),
718
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: l }, l)) }),
719
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
720
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
721
- "input",
722
- {
723
- id: "new-template-subject",
724
- style: styles3.input,
725
- value: subject,
726
- onChange: (e) => setSubject(e.target.value)
727
- }
728
- ),
729
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
730
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
731
- "textarea",
732
- {
733
- id: "new-template-html",
734
- style: { ...styles3.textarea, minHeight: 160 },
735
- value: html,
736
- onChange: (e) => setHtml(e.target.value)
737
- }
738
- ),
739
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
740
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
741
- "textarea",
742
- {
743
- id: "new-template-text",
744
- style: { ...styles3.textarea, minHeight: 100 },
745
- value: text,
746
- onChange: (e) => setText(e.target.value)
747
- }
748
- ),
749
- error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: error }) : null,
750
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
851
+ ),
852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: l }, l)) }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
854
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
855
+ "input",
856
+ {
857
+ id: "new-template-subject",
858
+ style: styles3.input,
859
+ value: subject,
860
+ onChange: (e) => setSubject(e.target.value)
861
+ }
862
+ ),
863
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
865
+ "textarea",
866
+ {
867
+ id: "new-template-html",
868
+ style: { ...styles3.textarea, minHeight: 160 },
869
+ value: html,
870
+ onChange: (e) => setHtml(e.target.value)
871
+ }
872
+ ),
873
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
875
+ "textarea",
876
+ {
877
+ id: "new-template-text",
878
+ style: { ...styles3.textarea, minHeight: 100 },
879
+ value: text,
880
+ onChange: (e) => setText(e.target.value)
881
+ }
882
+ ),
883
+ untranslated.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.notice, children: tr("templates.create.untranslated", { fields: untranslated.join(", ") }) }) : null,
884
+ error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: error }) : null,
885
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
886
+ ] }),
887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.previewColumn, children: [
888
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.previewHeader, children: [
889
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles3.label, children: tr("templates.editor.preview") }),
890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles3.meta, children: isPreviewing ? tr("templates.editor.rendering") : tr("templates.editor.live") })
891
+ ] }),
892
+ previewError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: previewError.explanation }) : null,
893
+ preview?.subject ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.previewSubject, children: preview.subject }) : null,
894
+ preview?.html ? (
895
+ // sandbox="" — no scripts, no same-origin: operator-authored HTML must
896
+ // never execute in the console's origin.
897
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
898
+ "iframe",
899
+ {
900
+ title: tr("templates.editor.previewTitle"),
901
+ style: styles3.previewFrame,
902
+ sandbox: "",
903
+ srcDoc: preview.html
904
+ }
905
+ )
906
+ ) : preview ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { style: styles3.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: tr("templates.editor.nothingRendered") })
907
+ ] })
751
908
  ] })
752
909
  ] });
753
910
  }
754
911
  var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
755
912
  var styles3 = {
756
- container: { padding: "8px 40px 64px", maxWidth: 760, boxSizing: "border-box" },
913
+ container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
914
+ // The editor's layout: the form on the left, the render pinned on the right.
915
+ split: {
916
+ display: "grid",
917
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
918
+ gap: 24,
919
+ alignItems: "start"
920
+ },
921
+ previewColumn: {
922
+ minWidth: 0,
923
+ display: "flex",
924
+ flexDirection: "column",
925
+ position: "sticky",
926
+ top: 16
927
+ },
928
+ previewHeader: { display: "flex", justifyContent: "space-between", alignItems: "center" },
929
+ previewSubject: { fontSize: 14, fontWeight: 600, margin: "8px 0" },
930
+ previewFrame: {
931
+ boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))",
932
+ width: "100%",
933
+ height: 520,
934
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
935
+ borderRadius: 8,
936
+ background: "var(--fonderie-surface,#fff)"
937
+ },
938
+ previewText: {
939
+ whiteSpace: "pre-wrap",
940
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
941
+ borderRadius: 8,
942
+ padding: 12,
943
+ fontSize: 13,
944
+ fontFamily: MONO
945
+ },
946
+ // The console's warning token (the shell's amber), readable in both themes.
947
+ notice: {
948
+ fontSize: 13,
949
+ color: "var(--fonderie-text,#171717)",
950
+ background: "color-mix(in srgb, var(--fonderie-warning,#f5a623) 16%, transparent)",
951
+ borderRadius: 6,
952
+ padding: "8px 12px",
953
+ margin: "12px 0 0"
954
+ },
757
955
  title: {
758
956
  fontSize: 22,
759
957
  fontWeight: 600,