@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.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/screens/TemplateEditorScreen.tsx
2
2
  import {
3
3
  createAdminT,
4
- formatAdminDate
4
+ formatAdminDate,
5
+ groupTemplatesByType,
6
+ missingTemplateLocales
5
7
  } from "@fonderie/client";
6
8
  import {
7
9
  useTemplate,
@@ -21,11 +23,13 @@ function TemplateEditorScreen({
21
23
  system = false,
22
24
  onDeleted,
23
25
  onAddLocale,
26
+ onSelectLocale,
24
27
  uiLocale
25
28
  }) {
26
29
  const t = useMemo(() => createAdminT(uiLocale), [uiLocale]);
27
30
  const { template, isLoading, error, refresh } = useTemplate(client, type, locale);
28
- const { saveTemplate, removeTemplate } = useTemplates(client);
31
+ const { templates, saveTemplate, removeTemplate } = useTemplates(client);
32
+ const siblings = groupTemplatesByType(templates).find((g) => g.type === type)?.entries ?? [];
29
33
  const [isDeleting, setIsDeleting] = useState(false);
30
34
  const { revisions, rollback } = useTemplateRevisions(client, type, locale);
31
35
  const [isSaving, setIsSaving] = useState(false);
@@ -96,6 +100,10 @@ function TemplateEditorScreen({
96
100
  setIsSaving(false);
97
101
  }
98
102
  };
103
+ const switchTo = (entry) => {
104
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
105
+ onSelectLocale?.(entry);
106
+ };
99
107
  const handleDelete = async () => {
100
108
  const message = locale ? t("templates.editor.confirmDeleteLocale", { type, locale }) : t("templates.editor.confirmDelete", { type });
101
109
  if (!window.confirm(message)) return;
@@ -114,18 +122,40 @@ function TemplateEditorScreen({
114
122
  if (error)
115
123
  return /* @__PURE__ */ jsx("p", { style: styles.error, role: "alert", children: error.explanation });
116
124
  return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
117
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
118
- /* @__PURE__ */ jsx("h1", { style: styles.title, children: type }),
119
- onAddLocale && !locale ? /* @__PURE__ */ jsx(
125
+ /* @__PURE__ */ jsx("h1", { style: styles.title, children: type }),
126
+ onSelectLocale || onAddLocale ? /* @__PURE__ */ jsxs("div", { style: styles.tabBar, children: [
127
+ onSelectLocale ? (
128
+ // Each tab opens another stored row, so this is navigation, not an
129
+ // ARIA tablist: plain buttons, the open one marked aria-current.
130
+ /* @__PURE__ */ jsx("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children: siblings.map((entry) => {
131
+ const selected = entry.locale === (locale ?? null);
132
+ return /* @__PURE__ */ jsx(
133
+ "button",
134
+ {
135
+ type: "button",
136
+ "aria-current": selected ? "true" : void 0,
137
+ onClick: () => selected ? void 0 : switchTo(entry),
138
+ style: selected ? styles.tabSelected : entry.active ? styles.tab : styles.tabInactive,
139
+ title: entry.active ? void 0 : t("common.status.inactive"),
140
+ children: entry.locale ?? t("templates.defaultChip")
141
+ },
142
+ entry.locale ?? ""
143
+ );
144
+ }) })
145
+ ) : null,
146
+ onAddLocale ? /* @__PURE__ */ jsx(
120
147
  "button",
121
148
  {
122
149
  type: "button",
123
150
  style: { ...styles.rollbackButton, marginLeft: "auto" },
124
- onClick: () => onAddLocale(type),
151
+ onClick: () => {
152
+ if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
153
+ onAddLocale(type, { locales: missingTemplateLocales(templates, type) });
154
+ },
125
155
  children: t("templates.editor.addLocale")
126
156
  }
127
157
  ) : null
128
- ] }),
158
+ ] }) : null,
129
159
  /* @__PURE__ */ jsxs("p", { style: styles.meta, children: [
130
160
  /* @__PURE__ */ jsx("strong", { children: locale ?? t("templates.defaultLocale") }),
131
161
  " \xB7 v",
@@ -286,8 +316,38 @@ function TemplateEditorScreen({
286
316
  ] })
287
317
  ] });
288
318
  }
319
+ var tab = {
320
+ height: 32,
321
+ padding: "0 10px",
322
+ background: "none",
323
+ border: "none",
324
+ borderBottom: "2px solid transparent",
325
+ color: "var(--fonderie-text-muted,#5c5c5c)",
326
+ fontSize: 13,
327
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
328
+ cursor: "pointer"
329
+ };
289
330
  var styles = {
290
331
  container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
332
+ // One tab per locale of this email, underlined like a document's tabs.
333
+ tabBar: {
334
+ display: "flex",
335
+ alignItems: "flex-end",
336
+ gap: 12,
337
+ borderBottom: "1px solid var(--fonderie-border,#e0e0e0)",
338
+ margin: "4px 0 12px",
339
+ paddingBottom: 6
340
+ },
341
+ tabs: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: -7 },
342
+ tab,
343
+ tabSelected: {
344
+ ...tab,
345
+ color: "var(--fonderie-text,#171717)",
346
+ fontWeight: 600,
347
+ borderBottomColor: "var(--fonderie-text,#171717)",
348
+ cursor: "default"
349
+ },
350
+ tabInactive: { ...tab, opacity: 0.55, textDecoration: "line-through" },
291
351
  // Two columns, always: the editor on the left, the rendered result on the
292
352
  // right. Stacking the preview under a long form hid it below the fold.
293
353
  split: {
@@ -456,7 +516,8 @@ var styles = {
456
516
 
457
517
  // src/screens/TemplateListScreen.tsx
458
518
  import {
459
- createAdminT as createAdminT2
519
+ createAdminT as createAdminT2,
520
+ groupTemplatesByType as groupTemplatesByType2
460
521
  } from "@fonderie/client";
461
522
  import { useTemplates as useTemplates2 } from "@fonderie/react-courier-admin";
462
523
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -468,6 +529,7 @@ function TemplateListScreen({
468
529
  }) {
469
530
  const t = createAdminT2(locale);
470
531
  const { templates, isLoading, error } = useTemplates2(client);
532
+ const groups = groupTemplatesByType2(templates);
471
533
  return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
472
534
  /* @__PURE__ */ jsxs2(
473
535
  "div",
@@ -492,23 +554,39 @@ function TemplateListScreen({
492
554
  }
493
555
  ),
494
556
  /* @__PURE__ */ jsx2("p", { style: styles2.hint, children: t("templates.list.hint") }),
495
- isLoading ? /* @__PURE__ */ jsx2("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ jsx2("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsx2("ul", { style: styles2.list, children: templates.map((template) => /* @__PURE__ */ jsx2("li", { style: styles2.row, children: /* @__PURE__ */ jsxs2(
496
- "button",
497
- {
498
- type: "button",
499
- onClick: () => onSelectTemplate?.(template),
500
- style: styles2.rowButton,
501
- children: [
502
- /* @__PURE__ */ jsx2("span", { style: styles2.type, children: template.type }),
503
- /* @__PURE__ */ jsx2("span", { style: styles2.locale, children: template.locale ?? t("templates.defaultLocale") }),
504
- template.system ? /* @__PURE__ */ jsx2("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : null,
505
- /* @__PURE__ */ jsxs2("span", { style: template.active ? styles2.active : styles2.inactive, children: [
506
- /* @__PURE__ */ jsx2("span", { style: styles2.dot }),
507
- template.active ? t("common.status.active") : t("common.status.inactive")
508
- ] })
509
- ]
510
- }
511
- ) }, `${template.type}:${template.locale ?? "base"}`)) })
557
+ isLoading ? /* @__PURE__ */ jsx2("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ jsx2("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsx2("ul", { style: styles2.list, children: groups.map((group) => /* @__PURE__ */ jsxs2("li", { style: styles2.row, children: [
558
+ /* @__PURE__ */ jsxs2(
559
+ "button",
560
+ {
561
+ type: "button",
562
+ onClick: () => onSelectTemplate?.(group.primary),
563
+ style: styles2.rowButton,
564
+ children: [
565
+ /* @__PURE__ */ jsx2("span", { style: styles2.type, children: group.type }),
566
+ group.system ? /* @__PURE__ */ jsx2("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : null,
567
+ /* @__PURE__ */ jsxs2("span", { style: group.primary.active ? styles2.active : styles2.inactive, children: [
568
+ /* @__PURE__ */ jsx2("span", { style: styles2.dot }),
569
+ group.primary.active ? t("common.status.active") : t("common.status.inactive")
570
+ ] })
571
+ ]
572
+ }
573
+ ),
574
+ /* @__PURE__ */ jsx2("span", { style: styles2.chips, children: group.entries.map((entry) => {
575
+ const name = entry.locale ?? t("templates.defaultChip");
576
+ return /* @__PURE__ */ jsx2(
577
+ "button",
578
+ {
579
+ type: "button",
580
+ onClick: () => onSelectTemplate?.(entry),
581
+ style: entry.active ? styles2.chip : styles2.chipInactive,
582
+ "aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
583
+ title: entry.active ? void 0 : t("common.status.inactive"),
584
+ children: name
585
+ },
586
+ entry.locale ?? ""
587
+ );
588
+ }) })
589
+ ] }, group.type)) })
512
590
  ] });
513
591
  }
514
592
  var pill = {
@@ -522,6 +600,18 @@ var pill = {
522
600
  lineHeight: 1.5,
523
601
  minWidth: 64
524
602
  };
603
+ var chip = {
604
+ height: 24,
605
+ boxSizing: "border-box",
606
+ padding: "0 8px",
607
+ borderRadius: 6,
608
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
609
+ background: "var(--fonderie-surface,#fff)",
610
+ color: "var(--fonderie-text,#171717)",
611
+ fontSize: 12,
612
+ fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)',
613
+ cursor: "pointer"
614
+ };
525
615
  var styles2 = {
526
616
  container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
527
617
  title: {
@@ -544,9 +634,16 @@ var styles2 = {
544
634
  overflow: "hidden",
545
635
  boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
546
636
  },
547
- row: { borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)" },
637
+ row: {
638
+ display: "flex",
639
+ alignItems: "center",
640
+ gap: 8,
641
+ paddingRight: 16,
642
+ borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)"
643
+ },
548
644
  rowButton: {
549
- width: "100%",
645
+ flex: 1,
646
+ minWidth: 0,
550
647
  display: "flex",
551
648
  justifyContent: "space-between",
552
649
  alignItems: "center",
@@ -565,7 +662,9 @@ var styles2 = {
565
662
  flex: 1,
566
663
  fontFamily: 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)'
567
664
  },
568
- locale: { fontSize: 12.5, color: "var(--fonderie-text-muted,#5c5c5c)" },
665
+ chips: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: 6 },
666
+ chip,
667
+ chipInactive: { ...chip, opacity: 0.5, textDecoration: "line-through" },
569
668
  dot: { width: 6, height: 6, borderRadius: 999, background: "currentColor" },
570
669
  builtIn: {
571
670
  fontSize: 11,
@@ -607,10 +706,13 @@ import {
607
706
  FonderieApiError,
608
707
  createAdminT as createAdminT3
609
708
  } from "@fonderie/client";
610
- import { useEffect as useEffect2, useState as useState2 } from "react";
709
+ import { useTemplatePreview as useTemplatePreview2 } from "@fonderie/react-courier-admin";
710
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
611
711
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
612
712
  var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
613
713
  var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
714
+ var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
715
+ var PREVIEW_DEBOUNCE_MS2 = 500;
614
716
  function TemplateCreateScreen({
615
717
  client,
616
718
  type,
@@ -627,14 +729,48 @@ function TemplateCreateScreen({
627
729
  const [text, setText] = useState2("");
628
730
  const [error, setError] = useState2(null);
629
731
  const [busy, setBusy] = useState2(false);
732
+ const [source, setSource] = useState2(
733
+ null
734
+ );
735
+ const { preview, isPreviewing, error: previewError, renderPreview } = useTemplatePreview2(client);
736
+ const variables = useRef2([]);
630
737
  useEffect2(() => {
631
738
  if (!type) return;
632
739
  client.getTemplate(type, null).then(({ result }) => {
633
740
  setSubject(result.subject ?? "");
634
741
  setHtml(result.html ?? "");
635
742
  setText(result.text);
743
+ setSource({ subject: result.subject ?? "", html: result.html ?? "", text: result.text });
636
744
  }).catch(() => void 0);
637
745
  }, [client, type]);
746
+ const previewType = addingLocale ? type : newType.trim();
747
+ const previewLocale = LOCALE_RE.test(targetLocale.trim()) ? targetLocale.trim() : null;
748
+ useEffect2(() => {
749
+ if (!TYPE_RE.test(previewType) || !text.trim()) return;
750
+ const timer = setTimeout(async () => {
751
+ const input = { text, ...subject ? { subject } : {}, ...html ? { html } : {} };
752
+ const render = () => renderPreview(
753
+ previewType,
754
+ { ...input, data: Object.fromEntries(variables.current.map((v) => [v, v])) },
755
+ previewLocale
756
+ );
757
+ try {
758
+ const result = await render();
759
+ const used = result.variables.filter((v) => !IMPLICIT2.has(v));
760
+ if (used.some((v) => !variables.current.includes(v))) {
761
+ variables.current = used;
762
+ await render();
763
+ }
764
+ } catch {
765
+ }
766
+ }, PREVIEW_DEBOUNCE_MS2);
767
+ return () => clearTimeout(timer);
768
+ }, [renderPreview, previewType, previewLocale, subject, html, text]);
769
+ const untranslated = source ? [
770
+ subject && subject === source.subject ? tr("templates.subject") : null,
771
+ html && html === source.html ? tr("templates.htmlBody") : null,
772
+ text === source.text ? tr("templates.textBody") : null
773
+ ].filter((f) => f !== null) : [];
638
774
  const submit = async (e) => {
639
775
  e.preventDefault();
640
776
  setError(null);
@@ -670,75 +806,140 @@ function TemplateCreateScreen({
670
806
  return /* @__PURE__ */ jsxs3("div", { style: styles3.container, children: [
671
807
  /* @__PURE__ */ jsx3("h1", { style: styles3.title, children: addingLocale ? tr("templates.create.titleLocale", { type }) : tr("templates.create.titleNew") }),
672
808
  /* @__PURE__ */ jsx3("p", { style: styles3.meta, children: addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew") }),
673
- /* @__PURE__ */ jsxs3("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
674
- !addingLocale ? /* @__PURE__ */ jsxs3(Fragment, { children: [
675
- /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
809
+ /* @__PURE__ */ jsxs3("div", { style: styles3.split, children: [
810
+ /* @__PURE__ */ jsxs3("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
811
+ !addingLocale ? /* @__PURE__ */ jsxs3(Fragment, { children: [
812
+ /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
813
+ /* @__PURE__ */ jsx3(
814
+ "input",
815
+ {
816
+ id: "new-template-type",
817
+ style: styles3.input,
818
+ value: newType,
819
+ placeholder: "weekly-digest",
820
+ autoComplete: "off",
821
+ spellCheck: false,
822
+ onChange: (e) => setNewType(e.target.value)
823
+ }
824
+ )
825
+ ] }) : null,
826
+ /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
676
827
  /* @__PURE__ */ jsx3(
677
828
  "input",
678
829
  {
679
- id: "new-template-type",
830
+ id: "new-template-locale",
680
831
  style: styles3.input,
681
- value: newType,
682
- placeholder: "weekly-digest",
832
+ value: targetLocale,
833
+ placeholder: "fr",
834
+ list: "new-template-locales",
683
835
  autoComplete: "off",
684
836
  spellCheck: false,
685
- onChange: (e) => setNewType(e.target.value)
837
+ onChange: (e) => setTargetLocale(e.target.value)
686
838
  }
687
- )
688
- ] }) : null,
689
- /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
690
- /* @__PURE__ */ jsx3(
691
- "input",
692
- {
693
- id: "new-template-locale",
694
- style: styles3.input,
695
- value: targetLocale,
696
- placeholder: "fr",
697
- list: "new-template-locales",
698
- autoComplete: "off",
699
- spellCheck: false,
700
- onChange: (e) => setTargetLocale(e.target.value)
701
- }
702
- ),
703
- /* @__PURE__ */ jsx3("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ jsx3("option", { value: l }, l)) }),
704
- /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
705
- /* @__PURE__ */ jsx3(
706
- "input",
707
- {
708
- id: "new-template-subject",
709
- style: styles3.input,
710
- value: subject,
711
- onChange: (e) => setSubject(e.target.value)
712
- }
713
- ),
714
- /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
715
- /* @__PURE__ */ jsx3(
716
- "textarea",
717
- {
718
- id: "new-template-html",
719
- style: { ...styles3.textarea, minHeight: 160 },
720
- value: html,
721
- onChange: (e) => setHtml(e.target.value)
722
- }
723
- ),
724
- /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
725
- /* @__PURE__ */ jsx3(
726
- "textarea",
727
- {
728
- id: "new-template-text",
729
- style: { ...styles3.textarea, minHeight: 100 },
730
- value: text,
731
- onChange: (e) => setText(e.target.value)
732
- }
733
- ),
734
- error ? /* @__PURE__ */ jsx3("p", { style: styles3.error, role: "alert", children: error }) : null,
735
- /* @__PURE__ */ jsx3("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
839
+ ),
840
+ /* @__PURE__ */ jsx3("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ jsx3("option", { value: l }, l)) }),
841
+ /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
842
+ /* @__PURE__ */ jsx3(
843
+ "input",
844
+ {
845
+ id: "new-template-subject",
846
+ style: styles3.input,
847
+ value: subject,
848
+ onChange: (e) => setSubject(e.target.value)
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
852
+ /* @__PURE__ */ jsx3(
853
+ "textarea",
854
+ {
855
+ id: "new-template-html",
856
+ style: { ...styles3.textarea, minHeight: 160 },
857
+ value: html,
858
+ onChange: (e) => setHtml(e.target.value)
859
+ }
860
+ ),
861
+ /* @__PURE__ */ jsx3("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
862
+ /* @__PURE__ */ jsx3(
863
+ "textarea",
864
+ {
865
+ id: "new-template-text",
866
+ style: { ...styles3.textarea, minHeight: 100 },
867
+ value: text,
868
+ onChange: (e) => setText(e.target.value)
869
+ }
870
+ ),
871
+ untranslated.length > 0 ? /* @__PURE__ */ jsx3("p", { style: styles3.notice, children: tr("templates.create.untranslated", { fields: untranslated.join(", ") }) }) : null,
872
+ error ? /* @__PURE__ */ jsx3("p", { style: styles3.error, role: "alert", children: error }) : null,
873
+ /* @__PURE__ */ jsx3("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
874
+ ] }),
875
+ /* @__PURE__ */ jsxs3("div", { style: styles3.previewColumn, children: [
876
+ /* @__PURE__ */ jsxs3("div", { style: styles3.previewHeader, children: [
877
+ /* @__PURE__ */ jsx3("span", { style: styles3.label, children: tr("templates.editor.preview") }),
878
+ /* @__PURE__ */ jsx3("span", { style: styles3.meta, children: isPreviewing ? tr("templates.editor.rendering") : tr("templates.editor.live") })
879
+ ] }),
880
+ previewError ? /* @__PURE__ */ jsx3("p", { style: styles3.error, role: "alert", children: previewError.explanation }) : null,
881
+ preview?.subject ? /* @__PURE__ */ jsx3("p", { style: styles3.previewSubject, children: preview.subject }) : null,
882
+ preview?.html ? (
883
+ // sandbox="" — no scripts, no same-origin: operator-authored HTML must
884
+ // never execute in the console's origin.
885
+ /* @__PURE__ */ jsx3(
886
+ "iframe",
887
+ {
888
+ title: tr("templates.editor.previewTitle"),
889
+ style: styles3.previewFrame,
890
+ sandbox: "",
891
+ srcDoc: preview.html
892
+ }
893
+ )
894
+ ) : preview ? /* @__PURE__ */ jsx3("pre", { style: styles3.previewText, children: preview.text }) : /* @__PURE__ */ jsx3("p", { style: styles3.meta, children: tr("templates.editor.nothingRendered") })
895
+ ] })
736
896
  ] })
737
897
  ] });
738
898
  }
739
899
  var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
740
900
  var styles3 = {
741
- container: { padding: "8px 40px 64px", maxWidth: 760, boxSizing: "border-box" },
901
+ container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
902
+ // The editor's layout: the form on the left, the render pinned on the right.
903
+ split: {
904
+ display: "grid",
905
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
906
+ gap: 24,
907
+ alignItems: "start"
908
+ },
909
+ previewColumn: {
910
+ minWidth: 0,
911
+ display: "flex",
912
+ flexDirection: "column",
913
+ position: "sticky",
914
+ top: 16
915
+ },
916
+ previewHeader: { display: "flex", justifyContent: "space-between", alignItems: "center" },
917
+ previewSubject: { fontSize: 14, fontWeight: 600, margin: "8px 0" },
918
+ previewFrame: {
919
+ boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))",
920
+ width: "100%",
921
+ height: 520,
922
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
923
+ borderRadius: 8,
924
+ background: "var(--fonderie-surface,#fff)"
925
+ },
926
+ previewText: {
927
+ whiteSpace: "pre-wrap",
928
+ border: "1px solid var(--fonderie-border,#e0e0e0)",
929
+ borderRadius: 8,
930
+ padding: 12,
931
+ fontSize: 13,
932
+ fontFamily: MONO
933
+ },
934
+ // The console's warning token (the shell's amber), readable in both themes.
935
+ notice: {
936
+ fontSize: 13,
937
+ color: "var(--fonderie-text,#171717)",
938
+ background: "color-mix(in srgb, var(--fonderie-warning,#f5a623) 16%, transparent)",
939
+ borderRadius: 6,
940
+ padding: "8px 12px",
941
+ margin: "12px 0 0"
942
+ },
742
943
  title: {
743
944
  fontSize: 22,
744
945
  fontWeight: 600,